- @sentry/symbolic 13.4.0 API surface: SourceBundleWriter for bundle-sources command: `@sentry/symbolic@13.4.0` exports 4 classes: `Archive`, `FileEntry`, `ObjectFile`, `SourceBundleWriter`, plus `SourceFileDescriptor`. Key for CLI source-tier commands: `SourceBundleWriter.writeObject(object: ObjectFile, object_name: string, filter: Function, provider: Function): Uint8Array | undefined` — callback-based; provider reads source content by path, filter selects files. `bundle-sources` is directly implementable (provider reads from disk). `print-sources` is BLOCKED — `ObjectFile` has no `sourceFiles()` enumeration method in 13.4.0 (only props: arch, codeId, debugId, fileFormat, hasDebugInfo, hasSources, hasSymbols, hasUnwindInfo, kind). `SourceFileDescriptor` has get/set props: contents, debugId, path, sourceMappingUrl, url, type. Confirmed by Dav1dde (Sebastian Zivota's colleague) on Jun 23 2026.
- Auth token env var override pattern: SENTRY_AUTH_TOKEN > SENTRY_TOKEN > SQLite: Auth token precedence in `src/lib/db/auth.ts`: `SENTRY_AUTH_TOKEN` > `SENTRY_TOKEN` > SQLite OAuth token. `getEnvToken()` trims env vars (empty/whitespace = unset). `AuthSource` tracks provenance. `ENV_SOURCE_PREFIX = "env:"` — use `.length` not hardcoded 4. Env tokens bypass refresh/expiry. `isEnvTokenActive()` guards auth commands. Logout must NOT clear stored auth when env token active. `runInteractiveLogin` catches OAuth flow errors internally and returns falsy on failure; login command sets `process.exitCode = 1` and returns normally (does NOT reject). Tests expecting `rejects.toThrow()` will fail — assert via fetch-call inspection instead. `requestDeviceCode` requires `SENTRY_CLIENT_ID` env var.
- Binary size breakdown: 94.5% is Node.js runtime — bundled code is ~6.3 MiB: Binary composition (linux-x64, Node 24 LTS): Node.js runtime=121 MiB (ships with debug symbols). `strip --strip-unneeded` → 99 MiB (-17 MiB raw, -4 MiB compressed). Strip built into fossilize 0.7.0 — happens on the copied binary BEFORE postject injection. After strip+SEA+binpunch: ~108 MiB raw, ~30 MiB gzip (vs 125 MiB / 34 MiB unstripped). .rodata=52.5 MB: V8 snapshot ~12 MB, ICU full-icu data ~28 MB. UPX compresses to 25 MiB but DESTROYS ELF notes — ruled out. `--with-intl=small-icu` saves ~26-28 MiB (biggest win from custom build); `--without-lief` BREAKS SEA; `--without-sqlite` BREAKS CLI; `--disable-single-executable-application` BREAKS EVERYTHING. Custom build deferred — poor cost/benefit (~3.5h build vs 5min fossilize). Final vs Bun: download 30 MiB (Bun: 32 MiB), `--version` ~1.0s (Bun: ~1.9s), completions ~150ms (Bun: ~180ms).
- bspatch.ts in-memory chain refactor: transformPatch callback + three public APIs: Core patching logic extracted into `transformPatch(oldFile, patchData, onChunk)` callback-based function. Three public APIs: `applyPatchToFile(oldPath, patchData, destPath)→SHA-256` (disk sink, final hop); `applyPatchToMemory(oldFile, patchData)→Uint8Array` (in-memory, intermediate hops); `applyPatchChainInMemory(oldPath, patches[], destPath)→SHA-256` (full chain orchestrator). `applyPatch()` kept as thin wrapper for backward compat. Orchestration lives in bspatch.ts (not delta-upgrade.ts) to keep buffer handling encapsulated. `onChunk` callback checks `writeError` flag set by writer 'error' event — throws immediately rather than waiting for top-of-loop check. `applyPatchToMemory` preallocates Uint8Array of `newSize`; corrupt patch claiming huge size throws RangeError → triggers full download fallback.
- bspatch.ts: TRDIFF10 patch application — inline SHA-256, streaming zstd, CoW old-file copy: TRDIFF10 header: 32 bytes (magic + controlLen + diffLen + newSize, all i64 LE). Control block decompressed fully via `zstdDecompressSync` (needs random access). Diff + extra blocks streamed via `createZstdStreamReader` (Node Transform → Web ReadableStream → `BufferedStreamReader`). `applyPatch()` ALWAYS computes SHA-256 inline and returns it — no separate verification step. `loadOldBinary()` copies to temp via `COPYFILE_FICLONE` (CoW reflink, falls back to regular copy) then reads into memory. `cleanupPatchResources()` runs all cleanup steps regardless of prior failures. Write errors captured early via `writer.on('error')` to avoid ERR_UNHANDLED_ERROR on ENOSPC/EIO.
- check-fragments.ts: validates fragment files against actual route names: `script/check-fragments.ts`: validates fragment files against actual route names (Check 1-4) AND validates subcommand coverage within fragments (Check 5). Check 5: for each route with >1 command, verifies fragment mentions each subcommand via a heading (outside fenced code blocks) or `sentry <route> <subcommand>` code reference. Default commands handled: if fragment contains bare `sentry <route>`, the default command is covered. Default commands detected from route map (`defaultCommand` field in route index files). Fenced code block content stripped before heading scan to avoid false positives from bash comments. Warnings by default; `--strict` makes them errors. Run via `pnpm run check:fragments`. CI `check-generated` job triggers when `changes.outputs.skill == 'true'`.
- check:stale-refs: generic toolchain consistency scanner derived from package.json: `script/check-stale-references.ts`: reads `packageManager` from `package.json` (e.g., `pnpm@10.11.0`), derives stale PMs dynamically, and scans dev-facing docs/scripts for stale `<pm> run`, `<pm> remove`, `<pm> add -d` commands and `requires <pm>`/`<pm> installed` prerequisite prose. Excludes: user-facing install instructions (fenced code blocks with `install -g`/`add -g`), the check script itself, and `node_modules/`. Added to CI lint job. **Generic**: if project migrates from pnpm to yarn, changing `packageManager` in `package.json` auto-flags all `pnpm run` references in dev docs — no manual pattern updates needed. Trap: script must exclude itself from scanning or its own JSDoc examples trigger false positives.
- issue list collapse=stats/lifetime API gotcha: SEEN_STATS_FIELDS, LIFETIME_FIELDS, buildListApiOptions: On the Sentry list endpoint, `collapse=stats` skips `_get_seen_stats()` entirely — stripping top-level `count`, `userCount`, `firstSeen`, `lastSeen` and the sparkline `stats` object (not just TREND). `src/commands/issue/list.ts`: `SEEN_STATS_FIELDS = new Set([...LIFETIME_FIELDS, 'stats'])`; `shouldCollapseForFields(fields, dependentFields)` shared by stats/lifetime decisions; `shouldCollapseStats(json, fields)` never collapses in human mode; JSON only when `--fields` omits all seen-stats fields. `buildListApiOptions(json, fields)`: `collapseLifetime = json && shouldCollapseForFields(fields, LIFETIME_FIELDS)`. `buildIssueListCollapse()` always starts with `['filtered','unhandled']`, conditionally adds `'lifetime'` then `'stats'`. `willShowTrend()` is display-only (hides TREND column on narrow/piped stdout). See #1219. NOTE: `count`/`userCount`/`firstSeen`/`lastSeen` always present on `issue view` (detail endpoint) — only potentially absent on `issue list` when collapse is active.
- Consola chosen as CLI logger with Sentry createConsolaReporter integration: Consola is the CLI logger with Sentry `createConsolaReporter` integration. Two reporters: FancyReporter (stderr) + Sentry structured logs. Level via `SENTRY_LOG_LEVEL`. `buildCommand` injects hidden `--log-level`/`--verbose` flags. `withTag()` creates independent instances; `setLogLevel()` propagates via registry. All user-facing output must use consola, not raw stderr. `HandlerContext` intentionally omits stderr. Telemetry opt-out priority: (1) `SENTRY_CLI_NO_TELEMETRY=1`, (2) `DO_NOT_TRACK=1`, (3) `metadata.defaults.telemetry`, (4) default on. Shell completions set `SENTRY_CLI_NO_TELEMETRY=1` in `bin.ts` before imports. Timing queued to `completion_telemetry_queue` SQLite table; normal runs drain via `DELETE ... RETURNING`. `ENV_VAR_REGISTRY` in `src/lib/env-registry.ts` is single source for all honored env vars; `topLevel: true` + `briefDescription` surfaces in `--help`. Add install-script-only vars with `installOnly: true`.
- Custom CA loading: priority, caching, TLS error detection, and SaaS warning: Custom CA in `src/lib/custom-ca.ts`: Priority: (1) `sentry cli defaults ca-cert` (SQLite), (2) `NODE_EXTRA_CA_CERTS`. Cached per-process via module-level vars (`hasResolved` flag). `resolve()` concatenates custom PEM with `rootCertificates` (additive — Bun replaces Mozilla bundle otherwise). `tryReadPem()` NEVER throws — missing CA file logs warn and returns `undefined`. `injectIntoNodeTls()` uses `tls.setDefaultCACertificates()` (Node 24+ only; no-op on Node 22). `TLS_ERROR_PATTERNS`: 5 patterns (local issuer, verify first cert, UNABLE_TO_VERIFY_LEAF_SIGNATURE, DEPTH_ZERO_SELF_SIGNED_CERT, SELF_SIGNED_CERT_IN_CHAIN) — explicitly excludes `CERT_HAS_EXPIRED` and `ERR_TLS_CERT_ALTNAME_INVALID`. `getTlsCertErrorMessage()` walks `error.cause` chain with cycle detection. SaaS target + env-sourced CA → one-time warning; stored default silences it. `__resetForTests()` resets all cached state.
- debug-files upload: per-file upload design and assemble body shape: The `sentry debug-files upload` command uses per-file upload (not per-slice). Assemble body shape: `{ [overallSha1]: { name, debug_id?, chunks: string[] } }`. Two modes: no-wait (stop once server holds chunks) and `--wait` (poll for `ok`/`error` up to `ASSEMBLE_MAX_WAIT_MS`). Filter rules mirror legacy `filter_features`. Auth deferred to `resolveOrgAndProject()` (standard cascade). Source bundles via `createSourceBundle` when `--include-sources`. Deduplication uses `debugId:sha1(content)` composite key. Early peek via `peekFormat()` in `prepareDifs` rejects non-DIF files before full read. Location: `src/lib/api/debug-files.ts`, `src/lib/dif/scan.ts`, `src/commands/debug-files/upload.ts`.
- delta-upgrade.ts: patch chain resolution and application architecture: Two channels: stable (GitHub Releases) and nightly (GHCR `patch-<version>` tags). Patch format: TRDIFF10 (zig-bsdiff + zstd). Constants: `MAX_STABLE_CHAIN_DEPTH=10`, `MAX_NIGHTLY_CHAIN_DEPTH=30`, `SIZE_THRESHOLD_RATIO=0.6`. Stable: single API call fetches releases with asset metadata, parallel `Promise.all` download. Nightly: list tags → filter semver range → fetch manifests → parallel blob download. `applyPatchesSequentially()` alternates between two intermediate files (`${destPath}.patching.a`/`.b`) — never read/write same path (mmap corruption). SHA-256 verified ONCE after all patches applied, not per-intermediate. Cache-first: `tryLoadCachedChain()` with key `patch-chain:{from}-{to}`. `canAttemptDelta()` blocks on dev version, cross-channel, or downgrade.
- embedded-ppdb: PE files are dropped, only extracted PPDB is uploaded: When scanning a managed PE (e.g. .NET assembly) with an embedded Portable PDB, `difFromCandidateBuffer` / `prepareFileDif` in `src/lib/dif/scan.ts` extracts the PPDB and returns it as a separate `PreparedDif` — the PE itself is dropped (featureless: no native debug info). Only the PPDB reaches the upload queue. This mirrors legacy `validate_dif` behavior which would reject featureless PEs anyway. The `--type portablepdb` filter is required to match; `--type pe` alone yields nothing for managed assemblies without native debug info.
- generate-docs-sections.ts: in-place marker injection into committed files: `script/generate-docs-sections.ts` (555+ lines): injects auto-generated content into committed files between named marker pairs. Marker styles: HTML `<!-- GENERATED:START name -->` (`.md`); MDX `{/* GENERATED:START name */}` (`.mdx`). `--check` flag: dry-run, exits 1 if stale. 13 sections across 5 files: `contributing.md` (project-structure, dev-prereq, build-commands), `DEVELOPMENT.md` (oauth-scopes, dev-env-vars, dev-prereq, build-toolchain), `self-hosted.md` (oauth-scopes, self-hosted-env-vars), `README.md` (dev-prereq, library-prereq, dev-scripts), `getting-started.mdx` (platform-support). Version extractors (`extractPnpmVersion`, `extractNodeVersion`) **throw on mismatch** — no silent fallbacks. No Bun references remain. CI `check-generated` job runs with `--check` flag.
- generate-docs-sections.ts: project-structure tree rendering invariant: In `generateProjectStructure()` (line 178 comment): groups (route directories) always use `├──` prefix regardless of position — because standalones always follow groups. Standalone entries include `help.ts` (added manually before sort); last standalone uses `└──`, others use `├──`. Both groups and standalones sorted alphabetically within their sections. Output is a fenced code block with `cli/` tree.
- generate:docs pipeline: 4-script sequence, prerequisites, and output ownership: Master orchestrator: `generate:docs` runs 4 scripts in sequence: (1) `generate:parser` → `script/generate-parser.ts`, (2) `generate:command-docs` → `script/generate-command-docs.ts`, (3) `generate:skill` → `script/generate-skill.ts`, (4) `generate:docs-sections` → `script/generate-docs-sections.ts`. Prerequisite for: `dev`, `build`, `build:all`, `bundle`, `typecheck`, `test:unit`, `test:changed`, `test:e2e`. Output ownership: `docs/src/content/docs/commands/` and `docs/src/content/docs/configuration.md` are gitignored (fully generated). `docs/src/fragments/` files are committed source of truth (hand-written custom content). `DEVELOPMENT.md`, `README.md`, `contributing.md`, `self-hosted.md`, `getting-started.mdx` are committed but have in-place injected sections between named markers.
- getsentry/cli skill system: generate-skill.ts outputs 4 artifacts, SKILL.md is auto-generated: `script/generate-skill.ts` (927 lines) generates skill files from Stricli CLI route tree introspection. Outputs: `plugins/sentry-cli/skills/sentry-cli/SKILL.md`, `plugins/sentry-cli/skills/sentry-cli/references/*.md` (26 files), `docs/public/.well-known/skills/index.json`, `src/generated/skill-content.ts`. The `skill-content.ts` embeds all skill files into the binary at build time so `agent-skills.ts` installs without network fetching. SKILL.md is auto-generated — never edit manually; regenerate with `pnpm run generate:docs`. `.cursor/skills/sentry-cli/` contains symlinks to `plugins/` location. Claude Code uses `.claude-plugin/marketplace.json` at repo root.
- getsentry/symbolic WASM architecture: zstd via C zstd-sys wasm-shim, self_cell ownership: WASM build uses C zstd via `zstd-sys` for ALL targets including `wasm32-unknown-unknown`. `zstd-sys` ships `wasm-shim/` with C headers; `build.rs` auto-enables shim for wasm32. CI `wasm-build` job installs `clang lld llvm`. ruzstd dropped (significantly slower per crate author + Sebastian Zivota). Ownership model: `self_cell`-based (`SelfCell<ByteView<'static>, di::Archive<'static>>`). `ObjectFile` rename fix: `#[wasm_bindgen(js_name = "ObjectFile")]`. Canonical field names use `.name()` method (lowercase: `elf`, `x86_64`) not `{:?}` Debug formatting. Smoke tests in `symbolic-wasm/npm/` so `npm test` works from npm dir. PR sequencing: symbolic PRs (#988, #992) must merge + republish before CLI `bundle-sources` PR.
- InkUI teardown order — 6 steps, all try/catch, torndown guard prevents double-unmount: `InkUI.tearDown()` must follow this order: (1) stop tip-rotation interval; (2) detach SIGINT listener + `store.setRequestCancel(undefined)`; (3) `instance.clear()`; (4) `instance.unmount()`; (5) restore alternate screen `\x1b[?1049l`; (6) `freshStdin.setRawMode(false)` + `.pause()` + `.destroy()`. `torndown: boolean` guard prevents double-unmount (throws on some platforms). `cancelRequested` guard: second Ctrl+C → `process.exit(130)`. Every step wrapped in try/catch.
- isSaaS() vs isSaaSTrustOrigin: different purposes, same URL source: In `src/lib/sentry-urls.ts`: `isSaaS()` (now exported) checks hostname only via `isSentrySaasUrl(getSentryBaseUrl())` — used for routing/UX decisions like `defaultIssueSort()`. `isSaaSTrustOrigin` is separate and requires https + default port — used for credential-trust decisions. JSDoc on `isSaaS()` explicitly points to `isSaaSTrustOrigin` for credential decisions. Trap: using `isSaaS()` for auth/credential gating looks correct but is wrong — it ignores scheme and port. `getConfiguredSentryUrl()` reads only env vars; `cli.ts` bootstrap (`preloadProjectContext`) injects stored SQLite default URL into `env.SENTRY_URL` before commands run, so `isSaaS()` sees self-hosted URLs correctly.
- isSentrySaasUrl vs isSaaSTrustOrigin: two intentional SaaS checks: `src/lib/sentry-urls.ts` exports two SaaS-detection helpers with intentional split: (1) `isSentrySaasUrl(url)` — hostname-only check (`sentry.io` or `*.sentry.io`), accepts any protocol/port. Used for routing/UX: custom-headers warning, `getSentryBaseUrl`/`isSelfHosted`, region resolution skip, telemetry `is_self_hosted` tag. (2) `isSaaSTrustOrigin(url)` — stricter: additionally requires `https:` and default port. Used for security decisions: token-host trust comparison, sentryclirc URL trust check, URL-arg trust, login refusal. Rule: hostname-only for routing/UX (don't break users behind TLS-terminating proxies with `http://sentry.io\`); strict for credential scoping. JSDoc on `isSentrySaasUrl` points callers to `isSaaSTrustOrigin` for security contexts. Keep both implementations in sync re: hostname matching.
- Issue list sort values: SortValue type, VALID_SORT_VALUES, getComparator, and SDK IssueSort: In `src/commands/issue/list.ts`: `SortValue` type (line 141, @internal) and `VALID_SORT_VALUES` array (line 143) are the CLI's local sort constraints. `IssueSort` in `src/lib/api/issues.ts` (lines 40–42) is derived from `@sentry/api` SDK via `NonNullable<NonNullable<ListAnOrganizationSissuesData['query']>['sort']>` — no API layer change needed when adding new sort values. SDK already includes `'date' | 'freq' | 'inbox' | 'new' | 'recommended' | 'trends' | 'user'`. To add a new sort value: (1) add to `SortValue` union and `VALID_SORT_VALUES`, (2) add case to `getComparator()` switch (default falls back to date), (3) update flag `brief` string, (4) change `default` if needed. `appendIssueFlags()` guards `--sort` omission only when `flags.sort !== 'date'` — update guard if default changes.
- maxZipTotalSize: 2GiB memory-safety budget separate from server maxFileSize policy: `PrepareDifsOptions.maxZipTotalSize` (default `DEFAULT_MAX_ZIP_TOTAL_SIZE` = 2GiB) is a cumulative uncompressed-extraction budget per `.zip` archive and a container size cap — it bounds peak decompression memory. It is distinct from `maxFileSize` (per-entry server upload policy). Commands do not need to pass `maxZipTotalSize` explicitly; `prepareDifs` applies the 2GiB default automatically. `0` disables the budget. Passed through `prepareZipDifs` → `readZipDifEntries` as `maxTotalSize`.
- Sentry CLI authenticated fetch architecture with response caching: Authenticated fetch + response cache: `createAuthenticatedFetch`: auth headers, 30s timeout, max 2 retries, 401 refresh, span tracing. `buildAttemptFactory` clones `Request`; do NOT materialize FormData (strips boundary). Per-endpoint timeout overrides (e.g. `/autofix/` 120s). Response cache RFC 7234 at `~/.sentry/cache/responses/`, GET 2xx only. TTL tiers: stable=5min, volatile=60s, immutable=24h. `@sentry/api` SDK passes Request with no init — undefined init → empty headers stripping Content-Type (HTTP 415); fall back to `input.headers` when init undefined. Guard `Array.isArray(data)` before `.map()` (SDK returns `{}` for 204/empty). Tests mocking fetch MUST call `useTestConfigDir()` + `setAuthToken()` + `resetCacheState()` + `disableResponseCache()` + `resetAuthenticatedFetch()` in beforeEach — GET response cache checked BEFORE fetch, so prior test cache hits produce 0 calls.
- Sentry CLI resolve-target cascade has 5 priority levels with env var support: Resolve-target cascade: (1) CLI flags, (2) SENTRY_ORG/SENTRY_PROJECT env vars, (3) SQLite defaults, (4) DSN auto-detection, (5) directory name inference. SENTRY_PROJECT supports `org/project` combo — SENTRY_ORG ignored if set. Schema v13 merged `defaults` table into `metadata` KV with keys `defaults.{org,project,telemetry,url}`; getters/setters in `src/lib/db/defaults.ts`. Prefer dedicated SQLite tables + migrations over `metadata` KV for non-trivial caches. Hidden global `--org`/`--project` flags: `mergeGlobalFlags()` in command.ts injects hidden flag shapes, `applyOrgProjectFlags()` writes to `SENTRY_ORG`/`SENTRY_PROJECT` before auth guard. No short aliases (`-p` conflicts). `@sentry/api` SDK: wrap types at `src/lib/api/*.ts` with `as unknown as SentryX` casts; never leak to commands. `unwrapResult`/`unwrapPaginatedResult` must stay CLI-owned. `apiRequestToRegion` auto-sets JSON Content-Type; `rawApiRequest` preserves strings.
- sentry-cli banner.ts: three-tier responsive banner with block-art wordmark: Banner lives in `src/lib/banner.ts`. Three tiers keyed on terminal columns: (1) Full (≥78 cols): `BANNER_ROWS_FULL` — 8 rows, 78 code points wide, arch logo + wordmark; (2) Wordmark-only (≥58 cols): `BANNER_ROWS_WORDMARK` — 8 rows, 58 code points wide; (3) Plain text (<58 cols): `BANNER_TEXT` (6 chars). `BANNER_GRADIENT`: 8 purple hex stops #b4a4de→#432b8a. `bannerLinesForWidth(columns)` is the single entry point — used by `help.ts` (via `process.stdout.columns ?? 80`) and `ink-ui.ts` (store seeded with FULL_BANNER_LINES; `IntroScreen` re-fits from `bodyWidth` every render). Block glyphs used: U+0020 + U+2580–U+259F subset. CJK ambiguous-width is an intrinsic limitation — decorative/TTY-gated, worst case cosmetic wrap, no crash. `?? 80` kept over `|| 80` (columns=0 → empty string, no wrap).
- sentry-cli docs site: brand asset locations and font stack: Brand asset locations: README uses `.github/assets/banner.png`; `docs/public/` holds `favicon.png`, `favicon.svg`, `og-image.png` (1200×630), `og-image-twitter.png` (1920×1080), `wordmark.svg`, `wordmark-light.svg`, `glyph.svg`; `docs/src/assets/` holds `logo-dark.svg`, `logo-light.svg`, `bg.png`, section-bg PNGs. `astro.config.mjs:52` references `./src/assets/logo.svg` for Starlight header; favicon set via `favicon: '/favicon.svg'` (PNG not referenced in built HTML). Font stack after PR #1183: headings → Dammit Sans (self-hosted OTF at `docs/src/fonts/dammit-sans-v0.3-bold.otf`); body → Rubik Variable (self-hosted via `@sentry/starlight-theme` fontsource); mono → IBM Plex Mono (theme) + JetBrains Mono (CDN). Accent color: `#7553FF` (`--sl-color-accent-rgb: 117 83 255`). Designer: Steven Lewis.
- sentry-cli skill install paths: ~/.claude/ and ~/.agents/ only — OpenCode is never a target: Skill source-of-truth: `plugins/sentry-cli/skills/sentry-cli/SKILL.md` (602 lines) + `references/` (28 per-command .md files). `installAgentSkills()` in `src/lib/agent-skills.ts` installs to `
/.agents/skills/sentry-cli/` and `/.claude/skills/sentry-cli/` only — no OpenCode path. OpenCode IS detected via `OPENCODE_CLIENT` env in `src/lib/detect-agent.ts` but only for telemetry, not skill installation. `.opencode/` and `opencode.json*` are gitignored (lines 72–74). Cursor symlinks live at `.cursor/skills/sentry-cli/` pointing into `plugins/`. OpenCode scans `/.claude/skills/**/SKILL.md` and `/.agents/**/SKILL.md` — `.cursor/` is NOT scanned. `installAgentSkills()` never creates top-level agent roots — their presence is the detection signal that the user already has a compatible agent installed. Skills are updated on every version bump (write-if-changed optimization is unnecessary). Writes use atomic rename: temp file `.<name>.<pid>.<rand>.tmp` in same dir → `rename()` into place, guaranteeing readers never observe a partial file.
- src/cli.ts: middleware chain, completion optimization, sensitive argv redaction: `src/cli.ts` exports `startCli()`, `runCli()`, `runCompletion()`. Middleware chain (innermost-first): `[seerTrialMiddleware, autoAuthMiddleware]` — auth is outermost. `autoAuthMiddleware` uses `isatty(0)` not `process.stdin.isTTY` (Bun returns undefined). `runCompletion()` sets `SENTRY_CLI_NO_TELEMETRY=1` to skip `@sentry/node-core` lazy-require (~280ms). `redactArgv()` handles `--flag=value` and `--flag <value>` forms; `SENSITIVE_ARGV_FLAGS` includes `token` and `auth-token`. `reportUnknownCommand()` wrapped in try/catch — telemetry must never crash CLI. `preloadProjectContext()` calls `captureEnvTokenHost()` BEFORE any env mutation.
- stdin-reopen.ts: forwardFreshTtyToStdin() idempotency and isTTY backfill pattern: `src/lib/init/stdin-reopen.ts` exports `forwardFreshTtyToStdin(deps?)` returning a `Disposable` (`TtyForwardingHandle`) — always non-null so callers use `using tty = forwardFreshTtyToStdin()` without null-checking. Idempotency: repeated calls return `NOOP_HANDLE` (secondary callers don't tear down primary's install). isTTY backfill: captures `previousIsTty` before touching; if `undefined`, uses `Object.defineProperty` to set `isTTY: true, writable: true, configurable: true` — required because Ink/clack gates `setRawMode(true)` on `input.isTTY`, so without backfill the fresh fd stays in canonical mode. `pause`/`resume` replaced with noops to prevent Bun kqueue EINVAL on fd-0 transitions. `TtyDeps` allows injection of `openTty` and `isTty` for test isolation.
- symbolic-wasm class-based API: Rc<Vec<u8>> ownership + on-demand re-parse pattern: symbolic-wasm ownership model: Dav1dde's PR #992 uses `SelfCell<ByteView<'static>, di::Archive<'static>>` — `self_cell`-based ownership, no re-parse. `derived_from_cell!` macro uses `std::mem::transmute` + `SelfCell::from_raw` to clone owner, letting `objects()` return owned `Object` cells sharing the same `ByteView`. PR #991 (Rc<Vec<u8>> + re-parse) closed in favor of this. `Object` getters: `debugId`, `codeId`, `arch`, `fileFormat`, `kind`, `hasSymbols`, `hasDebugInfo`, `hasUnwindInfo`, `hasSources`. `Archive` methods: `new(data)`, `peek(data)->Option<String>`, `fileFormat`, `objectCount`, `objects()->Result<Vec<ObjectFile>>`. CRITICAL: Rust struct named `Object` but exported as `ObjectFile` via `#[wasm_bindgen(js_name = "ObjectFile")]` — see gotcha entry for why.
- symbolic-wasm il2cpp WASM binding: free function + provider callback pattern: `il2cppLineMapping(object, provider)` is a free WASM function (not a method on `ObjectFile`) per Sebastian Zivota's preference. `provider` is a JS `Function` called with a path string; must return `Uint8Array` or `null`/`undefined`. `provider_bytes()` in `utils.rs` validates via `dyn_ref::<js_sys::Uint8Array>()` and throws a descriptive JS error for non-Uint8Array non-null values — `js_sys::Uint8Array::new` would silently zero-fill numbers or empty-fill plain objects. Returns `None` (JS `undefined`) when mapping is empty. `as_debuginfo()` is a `pub(crate)` non-wasm helper on `Object` so sibling modules can access the parsed object without re-exposing through the WASM API.
- Adopted workspace deps, cfg-zstd, required wasm-opt conventions from #989; kept #988 additions (serde_bytes: PR #988 (`feat/source-bundle-provider`, MERGED): `write_object_with_source_provider` + `write_object_with_filter` delegation. PR #989 (`fix/symbolic-followups`, MERGED, @sentry/symbolic@13.3.1): workspace deps, cfg-zstd, required wasm-opt conventions. PR #990 (C zstd on wasm, drop ruzstd, MERGED). PR #991 (`feat/wasm-api-classes`): CLOSED — superseded by Dav1dde's PR #992. PR #992 (`feat(wasm): Expose lower level API for debuginfo, add tests`, OPEN): Dav1dde's `self_cell`-based foundational API; +392/-101 across 11 files. Branch `prototype/wasm-artifact-smoke` (off #992 head `fd94b6fe`): adds artifact smoke test + `ObjectFile` rename fix; 5 files, +122/-6. SourceBundleWriter not yet in #992 — planned EOD Jun 23 2026. BYK will migrate `debug-files check` onto new API when republished.
- Banner art: direct bitmap grid read over area-averaging downsampling: Reference image (`sentry-ref.webp`, 2048×805px) is a SENTRY wordmark rendered from digit characters ('1'=on, '0'=off) in a monospace grid. Chosen approach: detect cell width via autocorrelation (~17.5px), read each cell on/off directly → 97×13 ASCII grid. Rejected: area-averaging downsampling (blended E's horizontal arms and R's counter/hole into solid fills, making letters unreadable); striped `▀` half-block rendering (50/50 duty-cycle dissolved E arms); solid `█` rendering (loses scanline texture). Post-processing: remove isolated cells (zero orthogonal neighbors) and small connected components to eliminate stray marks near Y's right arm.
- bspatch.ts: no mmap — Node has no native mmap API and native addons break SEA bundling: True mmap not viable: Node core has no `fs.mmap` (Buffer is always heap-backed); native addons (`mmap-io` etc.) break two hard project rules — no runtime dependencies and must bundle into Node SEA binary (esbuild can't bundle `.node` addons). `Bun.mmap` would have worked but project migrated off Bun. Alternative `pread` via `fs.read(fd, buf, 0, len, pos)` saves ~100MB JS heap for base binary (bytes stay in OS page cache) but cannot help intermediate hops — those buffers must stay in memory for in-memory chaining. Also: one big sequential `readFile` is faster than many small `pread`s. pread deferred as optional future optimization for single-patch case only.
- CLI source-tier: bundle-sources first, print-sources deferred pending source enumeration API: Chose to implement `bundle-sources` first over `print-sources` because `SourceBundleWriter.writeObject()` in `@sentry/symbolic@13.4.0` provides exactly the right shape (callback provider reads from disk). `print-sources` deferred because `ObjectFile` has no `sourceFiles()` enumeration method in 13.4.0 — blocked on Dav1dde shipping source enumeration in a future `@sentry/symbolic` release. WASM debug-files track is otherwise complete: symbolic PRs #988–#993 merged, `@sentry/symbolic@13.4.0` published, CLI PR #1124 merged.
- Decided to use job ID instead: decided to use job ID instead.
- Migrated to node: migrated from Bun to Node.
- Migrated to pnpm+node+vitest): migrated to pnpm+node+vitest).
- Node.js slim build flags for SEA binary size reduction: Node.js configure.py size-reduction flags for SEA builds: `--with-intl=small-icu` (English-only ICU, saves ~26-28 MiB — biggest win; CLI uses hardcoded en-US/sv-SE locales, safe); `--with-intl=none` (saves ~28-30 MiB but breaks `Intl.NumberFormat`/`String.normalize()` — NOT safe for this CLI); `--without-inspector` saves ~2-4 MiB; `--without-amaro` saves ~0.5 MiB; `--v8-disable-maglev` saves ~1-2 MiB; `--enable-lto` saves ~3-5 MiB. AVOID: `--without-ssl` (breaks HTTPS), `--without-lief` (BREAKS SEA), `--without-sqlite` (BREAKS CLI — uses node:sqlite), `--disable-single-executable-application` (BREAKS EVERYTHING), `--v8-lite-mode` (10x slower). Custom build deferred indefinitely — requires 5 native CI runners, ~3.5h cold build vs 5min fossilize. Cross-compilation from Linux to darwin NOT officially supported.
- Raw markdown output for non-interactive terminals, rendered for TTY: Markdown-first output pipeline: custom renderer in `src/lib/formatters/markdown.ts` walks `marked` tokens to produce ANSI-styled output. Commands build CommonMark using helpers (`mdKvTable()`, `mdRow()`, `colorTag()`, `escapeMarkdownCell()`, `safeCodeSpan()`) and pass through `renderMarkdown()`. `isPlainOutput()` precedence: `SENTRY_PLAIN_OUTPUT` > `NO_COLOR` > `FORCE_COLOR` > `!isTTY`. `--json` always outputs JSON. Colors defined in `COLORS` object in `colors.ts`. Tests run non-TTY so assertions match raw CommonMark; use `stripAnsi()` helper for rendered-mode assertions.
- Sixel banner approach deferred — block-art 56×8 wordmark chosen for now: User explicitly chose the 56×8 quadrant block-art wordmark over a sixel-based approach: 'Love the 56x8 let's roll with that. We can revisit sixel later.' Sixel is deferred as a future follow-up, not rejected permanently.
- symbolic-wasm API scope: general-purpose base, not CLI-specific shortcuts: Agreed with Dav1dde (getsentry/symbolic maintainer): `symbolic-wasm` may live in the symbolic repo only if it exposes a full general-purpose API base (analogous to the Python package), not CLI-focused shortcuts. CLI-specific logic (e.g. `collect_il2cpp` orchestration, source bundle writing with CLI semantics) must NOT live in the symbolic repo — move to getsentry/cli. Rationale: symbolic is a general-purpose library; CLI concerns create unwanted coupling. PR C (`feat/wasm-api-classes`): class-based API — `Archive` owns `Rc<Vec<u8>>`, caches metadata; `Object` caches fields at construction, re-reads debug session on demand for `source_files()`/`create_source_bundle()`. Callback-based wasm API uses `js_sys::Function` (getSource(path) → Uint8Array | null). Free functions `list_source_files`/`create_source_bundle` removed; `parse_debug_file`/`peek_format` kept for back-compat.
- @sentry/symbolic wasm-pack test never exercises the shipped artifact — use npm pack smoke test instead: Trap: `wasm-pack test` looks like the right way to test `@sentry/symbolic` — it builds and runs tests. But it builds its own glue code and never loads the `--target web` `symbolic.js` + `initSync` + package `exports`/`files` that consumers actually import. Fix: use a packaged-artifact smoke test: `npm pack` → install tarball into tmp dir → `import '@sentry/symbolic'` → assert via `node:test`. This catches exports-map/resolution breakage and runtime errors (e.g., the `Object` naming bug) that `wasm-pack test` misses entirely. Test files live in the package dir but are excluded from `files[]`; run via `cd symbolic-wasm/npm && npm test`.
- @sentry/symbolic: naming wasm export class `Object` shadows JS global — breaks Object.create and Object.getPrototypeOf: Trap: wasm-bindgen generates a JS class named after the Rust struct — naming it `Object` looks fine in Rust. But it shadows the JS global `Object` for the entire module: `Object.create` (used in `objects()`) and `Object.getPrototypeOf` (used in `initSync`) break with `TypeError: Object.getPrototypeOf is not a function`. Fix (PR #993): rename the export to `ObjectFile` via `#[wasm_bindgen(js_name = "ObjectFile")]` and `#[wasm_bindgen(js_class = "ObjectFile")]` — Rust struct name `Object` unchanged. Discovered via packaged-artifact smoke test, not `wasm-pack test`. Similarly, `ObjectDebugSession` needed `AsSelf` impl (PR #997) — check all new wasm-exposed types for missing `AsSelf` impls before using `derived_from_cell!`.
- @stricli/core -H alias patch: pin version to 1.2.7, never remove aliases: Trap: When Stricli throws on `-H` aliases used for `--header` or `--host`, removing the aliases from command files looks like the simple fix. But the project intentionally uses `-H` for curl-style API usage. Fix: the in-repo patch for @stricli/core (targeting 1.2.7) removes `-H` from the reserved list. Pin version to `1.2.7` (not `^1.2.8`) so the patch applies. Never remove `-H` alias usages from command files. Added in commit `78c9b04a5`. Cursor Bugbot and Seer both flag `-H` removal as blocking.
- `--require-all` false negatives for fat binaries — scan all matched objects: Trap: `missingRequestedIds` computed `foundIds` from only the primary object's `debugId` per file. Fat Mach-O with multiple slices causes non-primary slice IDs to be reported missing and exits 1 even though they were found. Fix: compute `foundIds` from all matched objects (`.objects` array), not just `selectBundledObject()?.debugId`. Applies to `src/commands/debug-files/upload.ts`.
- acquireLock ENOENT: parent directory may not exist — must mkdir before writeFileSync: Trap: `acquireLock(lockPath)` calls `writeFileSync(lockPath, pid, { flag: 'wx' })` — looks safe because the lock path is derived from the install path. But the parent directory (e.g. `~/.sentry/bin` or a stale `/tmp/sentry-test-install/`) may not exist, causing ENOENT (Sentry issue CLI-1E1). Fix: call `mkdirSync(dirname(lockPath), { recursive: true, mode: 0o755 })` BEFORE the try block in `acquireLock` (`src/lib/binary.ts`). CRITICAL: keep mkdir OUTSIDE the try block — if inside, mkdir's EEXIST error routes into `handleExistingLock`, obscuring root cause (comment claiming infinite recursion is inaccurate: actual path is EEXIST→handleExistingLock→readFileSync→ENOTDIR, single throw). The directory creation is idempotent — `recursive: true` is a no-op if it already exists. Merged in PR #1125 (squash `ee768454c`).
- Atomic-write test tautology: 'no temp files' test passes even without atomic behavior: Trap: a test that checks 'no `.tmp` files left behind after install' looks like it guards atomic write behavior. But if the non-atomic path (`writeFile` directly) never creates `.tmp` files in the first place, the `leftovers` filter is trivially empty and the test passes with the atomic code removed entirely — confirmed via mutation test on `byk/fix/agent-skills-atomic-write`. Fix: the test must exercise the overwrite/update path (where a concurrent reader could observe truncation) AND inject a fault (e.g., mock `rename` to throw) to verify cleanup. A test that only runs the happy-path fresh-install never touches the race condition the feature guards against. Per red-green directive: if the test passes with the guard deleted, it isn't testing the guard.
- batch-queue.ts: 404 from upstream treated as transient — provider never disabled: Trap: `BatchProvider.submit()` returns `null` for any non-401/403 HTTP error, including 404. `submitBatch()` treats `null` as transient and falls back — no disable happens. For providers that don't implement `/v1/messages/batches` (e.g. MiniMax), this causes a wasted HTTP round-trip every 30s forever. Fix: add `"not-found"` return value for 404 in both Anthropic and OpenAI submit methods. In `submitBatch()`, handle `"not-found"` with provider-level disable: add `disabledBatchProviders: Set<string>` (keyed by provider name), persist to `kv_meta` via `setKV()`, restore on startup. Add fast-path bypass in both `flush()` and `prompt()`. Provider-level (not per-session) because the URL is baked in at construction — one provider per process. `groupKey()` = `authFingerprint(cred)|providerID`; per-credential disable was removed in favor of per-session historically.
- Biome cognitive complexity cap of 15 — ZIP branch in prepareDifs pushed it to 17: Trap: adding a ZIP expansion branch to `prepareDifs` in `scan.ts` looks like a small addition. But Biome enforces a cognitive complexity cap of 15 — the ZIP branch pushed `prepareDifs` to 17. Fix: extract per-file processing into a private helper `prepareFileDif(path, filters, maxFileSize)` returning `{ dif: PreparedDif | null; oversized: boolean }`. Pattern: when adding conditional branches to existing complex functions, check Biome complexity score first and pre-extract helpers.
- Biome noParameterProperties: never use TypeScript constructor parameter properties in class definitions: Trap: TypeScript parameter properties (`constructor(private readonly handle: FileHandle)`) look like idiomatic shorthand and compile fine. But Biome enforces `noParameterProperties` and will error. Fix: always declare explicit class fields and assign in constructor body. Applies to all new classes in `src/lib/**/*.ts`. Caught during `FileOldReader`/`MemoryOldReader` addition in `bspatch.ts` — 4 Biome errors at lines 281, 310, 311, 312.
- Biome stdin check reports 'contents aren't fixed' false positive — use file path instead: Trap: running `biome check --stdin-file-path=<file>` and piping content exits with code 1 and 'contents aren't fixed' even when `--write` produces no diff — looks like a real formatting violation. Fix: run Biome directly on the file path (`biome check <file>`) rather than via stdin mode. The stdin mode false positive was confirmed during the `agent-skills.ts` review: exit code 1 from stdin, but `--write` produced zero changes. Always use file-path invocation for authoritative Biome results.
- build-binary job in ci.yml uses setup-node — not Bun-only as commonly assumed: Trap: `build-binary` job in `ci.yml` is assumed to use only Bun (not `setup-node`) because the binary build pipeline uses fossilize/esbuild. But `ci.yml` lines 261–263 contain an `actions/setup-node@v6` step inside `build-binary` — PR #1145 changed it from `node-version: "22"` to `${{ env.NODE_VERSION_22 }}`. The PR description incorrectly stated `build-binary` is unaffected. Fix: always grep `ci.yml` for `setup-node` rather than assuming job intent from its name. The Node install in `build-binary` is likely for pnpm/tooling, not the compiled artifact.
- bundle-sources.ts exit code 1 for no-sources relies on cli.ts never resetting exitCode — fragile invariant: Trap: using `this.process.exitCode = 1` directly (at `bundle-sources.ts:145`) instead of throwing `OutputError`(=60) looks like a valid shortcut. It works today because `cli.ts:622-649` never resets `exitCode` on a clean return. But this is a fragile invariant — if `cli.ts` ever resets `exitCode`, the no-sources case silently exits 0. The `OutputError`(=60) idiom is the correct pattern. The `exitCode=1` approach was kept for consistency with `check.ts` pattern; a comment was added explaining the choice. Consider migrating to `OutputError` in a future cleanup.
- check:fragments only validates file existence — not subcommand coverage depth: RESOLVED in PR #1024. `script/check-fragments.ts` now has Check 5: for each route with >1 command, verifies the fragment mentions each subcommand via a heading (outside fenced code blocks) or `sentry <route> <subcommand>` code reference. Default commands (e.g., `local serve`) are handled — if the fragment contains `sentry <route>` bare, the default command is considered covered. Default commands detected from route map `defaultCommand` field. Fenced code block content stripped before heading scan to avoid bash-comment false positives. Warnings (not errors) by default; `--strict` flag makes them errors.
- check.ts hasId() uses !== null but ObjectFile.codeId is string|undefined — null guard mismatch: Trap: `check.ts` `hasId()` guards `o.codeId !== null && o.codeId.length > 0`. Looks correct because old `DifObjectInfo.codeId` was `string | null`. But `ObjectFile.codeId` in `@sentry/symbolic` 13.4.0 is `string | undefined` — the getter returns `undefined` (not `null`) when Rust returns `None`. If `DifObjectInfo.codeId` is mapped as `string | null` (via `?? null`), the `!== null` guard works. If mapped as `string | undefined` (via `?? undefined`), the guard silently passes `undefined` through. Fix: ensure `parseDebugFile` maps `codeId` to `string | null` (using `obj.codeId ?? null`) so `check.ts`'s existing `!== null` guard remains correct without touching `check.ts`.
- chunk-upload gzip wire format: never emit Content-Encoding: gzip with file_gzip field: Trap: gzip-compressed chunks look like they should use `Content-Encoding: gzip` alongside the `file_gzip` multipart field — standard HTTP compression convention. But zstd-aware Sentry servers reject that combination with 400 to avoid ambiguity. Fix: gzip path uses ONLY the `file_gzip` multipart field with NO `Content-Encoding` header (legacy protocol). Zstd path uses `Content-Encoding: zstd` + `file` field (requires server opt-in via getsentry/sentry#113760+). This is a standing directive from code comments in `src/lib/api/chunk-upload.ts`.
- dashboard revisions/restore and issue events subcommands are undocumented in fragment files: RESOLVED in PR #1024. `docs/src/fragments/commands/dashboard.md` now documents `revisions` and `restore`. `docs/src/fragments/commands/issue.md` now documents `events` and `@latest`/`@most_frequent` selectors. `docs/src/fragments/commands/cli.md` now documents `defaults` and `import`. `check:fragments` (Check 5) now validates subcommand coverage within fragment files — not just file existence. When adding new subcommands, always update the corresponding fragment in `docs/src/fragments/commands/` AND run `pnpm run check:fragments` to verify coverage.
- debug-files upload: partial size-drop silently exits 0 — doUpload must receive oversizedCount: Trap: `doNothingToUpload` (all-dropped path) correctly calls `setExitCode(1)` when `oversizedCount > 0`, establishing the contract that oversized files → non-zero exit. But `doUpload` (partial-drop path) did not receive `oversizedCount`/`maxFileSize` — so uploading 3 files where 1 was oversized exited 0 silently. Fix (PR #1146): pass `oversizedCount` and `maxFileSize` to `doUpload`; append scan-oversize message to failures hint; call `setExitCode(1)` when `oversizedCount > 0` even with no upload failures. Warden finding 9LL-87A on PR #1140.
- delta-upgrade intermediate files: always alternate .patching.a/.b — never write to source path: Trap: writing patch output to the same path as input looks like a simple in-place update. Fix: `applyPatchesSequentially()` alternates between `${destPath}.patching.a` and `${destPath}.patching.b` because the old binary is mmap'd for reading — writing to the source path truncates it and corrupts output. Single-patch chains apply old→dest directly (safe, different paths). `cleanupIntermediates()` called in `finally` block removes both intermediates via `unlinkSync` (ignoring errors) — always runs even on failure. This is a standing directive: 'Always clean up intermediate files, even on failure' and 'never target the same path.'
- DEVELOPMENT.md hand-written prose is not covered by any staleness check: RESOLVED in PR #1024. `DEVELOPMENT.md` hand-written prose is now wrapped in `GENERATED:START/END` markers: `dev-prereq` (lines 4-7) and `build-toolchain` (lines 91-97). These sections are now auto-generated from `package.json` by `generate-docs-sections.ts` and validated by `check:docs-sections --check`. The only remaining non-generated prose in `DEVELOPMENT.md` is the OAuth app setup instructions and architecture description — these don't reference toolchain versions. `generate-docs-sections.ts` no longer contains any Bun references; `extractPnpmVersion()` and `extractNodeVersion()` throw on mismatch.
- difFromCandidateBuffer: PE entries must be allowed through format filter when portablepdb is requested: Trap: the format filter in `difFromCandidateBuffer` rejects PE files when `--type portablepdb` is specified, because `peekFormat` returns `'pe'` not `'portablepdb'`. But embedded PPDBs live inside PE files — blocking the PE at the format-filter stage means the PPDB is never extracted. Fix: when `filters.formats` includes `'portablepdb'`, also allow `'pe'` through the format gate so the parser can inspect the PE for an embedded PPDB. The PE itself is still dropped after extraction if it has no features.
- embeddedPpdbDif: oversized extracted PPDB dropped silently — oversizedCount not incremented: Trap: `embeddedPpdbDif()` (scan.ts:628-633) returns `null` when the decompressed PPDB exceeds `maxFileSize`, but does NOT increment `oversizedCount`. Looks correct because the PPDB is a derived artifact, not a scanned file. But the user gets no signal that a PPDB was found and dropped — silent data loss. The existing directive (PR #1146) requires `oversizedCount > 0` → non-zero exit and scan-oversize message. Fix: increment `oversizedCount` (or return a sentinel) when an extracted PPDB is size-gated out, so the caller can report it.
- error-reporting.ts: three independent 4xx classifiers must stay in sync — isUserError, classifySilenced, isUserApiError: Trap: `classifySilenced` (Sentry capture gate), `isUserError` in `errors.ts` (upgrade nudge via `getErrorUpdateNotification`), and `isUserApiError` in `telemetry.ts` (span attribution) all classify 4xx errors independently. Adding a new silence rule to only one leaves the others out of sync — e.g., query-parse 400s were silenced in `classifySilenced` but still triggered the upgrade nudge until `isUserError` was updated. Fix: whenever a new error classification is added (e.g., `isSearchQueryParseError`, `isNetworkError`), update all three classifiers. Export shared predicates from `errors.ts` so all three import the same function.
- eval-skill-fork.yml has no setup-node step — floating Node version bypasses pins: Trap: `.github/workflows/eval-skill-fork.yml` job `eval` has no `actions/setup-node` step at all — it runs on the runner image's system Node (e.g. 24.17.0/22.23.0), the exact versions the Node-pin PR (#1145) aimed to avoid. The PR's claim 'every setup-node step is pinned' is technically true but the intent is unmet because this job never calls setup-node. Fix: add a pinned `actions/setup-node@v6` step with `node-version: ${{ env.NODE_VERSION_24 }}` before `pnpm install` in the `eval` job. Discovered via subagent adversarial review of PR #1145.
- eval-skill-fork.yml missing setup-node — fork PRs exposed to Node regression: Trap: PR #1145 pinned Node versions in all 4 main workflow files and the PR description claimed completeness. But `.github/workflows/eval-skill-fork.yml` job `eval` (lines 27–57) has no `actions/setup-node` step — it runs on the runner's system Node. This is the exact workflow exhibiting `ERR_STREAM_PREMATURE_CLOSE` (CVE-2026-48931 regression in Node 24.17.0/22.23.0). Fix: add `actions/setup-node@v6` with `node-version: ${{ env.NODE_VERSION_24 }}` before `pnpm install` in that job. When pinning Node versions across CI, always grep ALL workflow files for `setup-node` AND check for jobs that invoke Node without an explicit setup step.
- existsSync+realpathSync TOCTOU: catch ENOENT instead: Trap: `if (!existsSync(p)) return resolve(p); return realpathSync(p)` looks safe but has a TOCTOU race. Also: `realpathSync` inside async is inconsistent. Fix: call `await realpath(p)` (node:fs/promises) directly; catch `ENOENT` to fall back to `resolve(p)`; log non-ENOENT errors via `logger.debug(msg, error)` before falling back. When mocking in vitest, mock `node:fs/promises` not `node:fs`. RELATED: In cleanup/unlink catch blocks, only log non-ENOENT errors — `ENOENT` during cleanup is expected. Pattern: `if ((error as NodeJS.ErrnoException).code !== 'ENOENT') logger.debug(msg, error)`. Pre-existing silent `catch { // Ignore }` blocks must be fixed to log non-ENOENT errors. Confirmed fixed in PR #1046 (`fix/install-binary-symlink-self-copy`).
- Frontify brand portal cannot be accessed programmatically — requires auth: Trap: `https://brand.getsentry.com/share/wLssCFiQ5ZzmQmKCWym4\` returns HTTP 200 and looks like a static asset server. It's a JS-rendered SPA — no static asset URLs are extractable from HTML. API probes (`/api/share/…`, `/api/shares/…`) return 404 or HTML. CDN URLs found embedded in the SPA shell (`media.ffycdn.net`) are portal chrome assets (OG illustration, favicon), not the actual brand files. Fix: use the pre-signed download endpoint `https://brand.getsentry.com/api/screen/download/\` — token must be extracted from the authenticated session or provided by the user. Always ask the user to provide Frontify asset URLs or download tokens directly.
- generate-docs-sections.ts still references Bun — extractBunVersion() silently falls back to hardcoded '1.3': RESOLVED in PR #1024. `generate-docs-sections.ts` previously had `BUN_VERSION_RE` and `extractBunVersion()` that silently returned hardcoded `'1.3'` when `packageManager` was `pnpm@10.11.0`. Fixed: replaced with `extractPnpmVersion()` and `extractNodeVersion()` that **throw on mismatch** instead of silently falling back. `generateDevPrereq()`, `generateDevPrereqContributing()`, `generateLibraryPrereq()` now reference Node.js + pnpm. `DEVELOPMENT.md` lines 5 and 91 are now wrapped in `GENERATED:START/END` markers so they can't drift again. No Bun references remain in the script.
- getCurlInstallPaths trusts stale stored install path — must guard with existsSync(dirname): Trap: `getCurlInstallPaths()` in `src/lib/upgrade.ts` reads the stored install path from SQLite and uses it directly — looks correct because the path was valid at install time. But macOS cleans `/tmp` on reboot, and users may delete test install dirs, leaving a stale DB entry. Fix: guard the stored-path branch with `existsSync(dirname(stored.path))` before trusting it; fall back to `process.execPath` startsWith-match against `KNOWN_CURL_DIRS` (`['.local/bin','bin','.sentry/bin']`), then `~/.sentry/bin` default. Conservative: only add the guard — do NOT prefer `execPath` over stored path (breaks npm→nightly migration flow). NFS edge case is self-resolving: if binary runs from NFS mount, mount must be active so `existsSync` passes.
- git add -A during rebase sweeps in stray untracked files and .lore.md conflicts: Trap: `git add -A` looks like a safe 'stage everything' shortcut during rebase conflict resolution. But it stages untracked stray files (e.g. `sentry-lightning-talk.md`) and auto-stages `.lore.md` conflict markers, polluting the commit. Fix: always stage specific file paths explicitly (e.g. `git add src/commands/debug-files/read-file.ts`) during rebase resolution. If the rebase is complex, abort with `git rebase --abort`, reset to `origin/main`, and re-apply edits manually.
- GitHub CI skips pull_request workflows entirely when PR has merge conflicts: Trap: missing CI jobs on a PR look like a workflow trigger bug or branch filter issue — easy to spend time investigating ci.yml triggers. Fix: check `mergeable`/`mergeStateStatus` first. When a PR is `CONFLICTING` (GitHub cannot compute the merge ref), ALL `pull_request`-triggered workflows are silently skipped — only `pull_request_target` and CodeQL/external checks still run. Confirmed on sentry-cli (TypeScript) PR #1123: full Build/lint/test suite absent because `mergeStateStatus: DIRTY`. Resolution: rebase or resolve conflicts, then CI triggers normally.
- isNetworkError must NOT match ApiError(status=0) — TLS cert errors share status 0: Trap: `ApiError` with `status === 0` looks like a network failure (no HTTP response received) and is tempting to include in `isNetworkError()`. But TLS certificate errors are also wrapped as `ApiError(status=0)` — once wrapped, `isTlsCertError()` cannot reliably re-detect them. Silencing all status-0 errors would suppress actionable TLS config issues. Fix: `isNetworkError()` matches ONLY `error instanceof TypeError && error.message === 'fetch failed'` (the unambiguous undici network signature). `ApiError(status=0)` is handled separately via an explicit `error.status === 0` branch in `isUserError()` with a comment explaining the TLS overlap. Callers that want status-0 treated as user-env check it explicitly.
- issue list --sort recommended: client-side re-sort must be skipped for single-project results: Trap: applying `getComparator('recommended')` to single-project results looks like consistent behavior. Fix: the `isMultiProject` guard at list.ts:1144-1148 is the ONLY client-side issue sort — it's intentionally skipped for single-project results because re-sorting would silently replace the server's recommended ranking with a `lastSeen` fallback. `getComparator('recommended')` returns `compareDates(a.lastSeen, b.lastSeen)` — same as `date` sort — so applying it to single-project results would corrupt the server's relevance ranking without any visible error.
- issue list sort default test coverage gap: tests always pass sort explicitly: Trap: `list.test.ts` passes `sort` explicitly in every `func.call(...)` invocation (`sort: 'date'` or `sort: 'recommended'`). No end-to-end test omits `sort` and asserts the API received the correct default (`recommended` on SaaS, `date` on self-hosted). This means `defaultIssueSort()` logic is not covered by integration tests — only unit-tested via `__testing` exports. When adding host-dependent flag defaults, always add a test that omits the flag and verifies the resolved default reaches the API call.
- listSources: never copy descriptor.contents across wasm boundary per-file — use type field instead: Trap: `descriptor.contents` looks like the right way to check if a source is embedded — it's the actual content. But reading it copies the full Rust String across the wasm↔JS boundary for every file, triggering the encoding mismatch dav1d flagged (Rust String vs JS String encoding). Fix: use `descriptor.type` (a cheap string tag: `'url'`, `'resolved'`, etc.) to classify the source without copying contents. `DifSourceFile` shape uses `type` field, not `embeddedBytes`. Only fetch `contents` if the caller explicitly needs the source text.
- loadOldBinary: new Uint8Array(readFile()) double-allocates — return Buffer directly: Trap: `new Uint8Array(await readFile(tempCopy))` looks like a safe type conversion. But `readFile()` already returns a `Buffer` which IS a `Uint8Array` — wrapping it in `new Uint8Array()` copies the entire ~100MB binary, causing a transient double-allocation peak. Fix: return the `Buffer` directly from `loadOldBinary` with no wrapper. Zero tradeoff — Buffer is already a Uint8Array subclass and works everywhere Uint8Array is expected.
- Local tarball paths in package.json break CI with pnpm install --frozen-lockfile: Trap: `file:/tmp/opencode/sentry-symbolic-new.tgz` in dependencies looks harmless locally — `pnpm install` works fine. But CI runs `pnpm install --frozen-lockfile`, which exits 254 when the tarball path doesn't exist. The lockfile also changes the specifier from published version to path, causing diffs on every install. Fix: always keep published version specifiers (e.g. `13.4.0`) in package.json. Use `npm pack` + separate install for local testing, or pnpm overrides.
- login.ts blind catch: bare catch around getUserRegions() mislabels network/server failures as invalid token: Trap: a bare `catch {}` around `getUserRegions()` in `src/commands/auth/login.ts` that always calls `clearAuth()` + throws `AuthError('invalid')` looks like correct token-validation error handling. But it conflates genuine 401/403 (bad token) with network errors, 5xx server errors, and parse failures — clearing a possibly-valid token and showing a misleading 'Invalid API token' message. Fix (PR #1153): extract `handleTokenValidationError()` helper — only clears auth and throws `AuthError('invalid')` for `ApiError` with status 401 or 403; re-throws original error for all other failures. `AuthError('invalid')` is now safe to silence in `classifySilenced` because it only fires on genuine auth rejections.
- MastraClient has no dispose API — use AbortController for cleanup: MastraClient has no `close()`/`dispose()` API — cleanup via `ClientOptions.abortSignal` (constructor) or per-prompt `signal`. Without explicit abort, Bun's fetch dispatcher keep-alive sockets hold the event loop alive past natural exit. Pattern in `src/lib/init/wizard-runner.ts`: create `AbortController` per `runWizard`, pass `abortSignal: controller.signal` to `new MastraClient(...)`, abort via `using _ = { [Symbol.dispose]: () => controller.abort() }`. Custom `fetch` wrapper must preserve `init.signal` via spread. Tests capture `ClientOptions` via `spyOn(MastraClient.prototype, 'getWorkflow').mockImplementation(function() { capturedOpts.push(this.options); ... })`.
- SUPERSEDED — org listing no longer fans out per region: The `Promise.allSettled`/`hasSuccessfulRegion`/`lastScopeError` pattern this entry used to document was removed from `listOrganizationsUncached` (`src/lib/api/organizations.ts`, PR #1212). `GET /organizations/` is now served from the control silo and returns every org across all regions in one call (getsentry/sentry#112622), so there's no more per-region 403/500 bookkeeping — a single request either succeeds with the full list or throws one enriched `ApiError`. `listOrganizationsPage` is the paginated primitive now (`listOrganizationsInRegion` was removed — no callers left); `getUserRegions` is kept only for token validation in `auth login`/sentryclirc import and is no longer part of the listing path.
- OOM on large non-DIF files — header peek before full read: Trap: `prepareDifs` reads entire file into memory before calling `parseDebugFile`. Large files that aren't DIFs (e.g. videos, logs) cause OOM. Fix: call `peekFormat` on a small header chunk first to reject non-object files early, then only read the full file if it passes. Pattern documented in `src/lib/dif/scan.ts` with `peek(16)` + `peekFormat(header)`.
- OpenCode memoizes skill list at session start — changes to SKILL.md not picked up until restart: Trap: modifying `~/.claude/skills/sentry-cli/SKILL.md` and seeing count=6 (sentry-cli absent) in the current session looks like a parse/load failure. Fix: OpenCode's `InstanceState.make` caches skill discovery once per instance at session start — `opencode debug skill` from a fresh invocation shows the true live count (7 including sentry-cli). Stale session snapshots always show the count from when the session started. To verify skill loading, always run `opencode debug skill` from a new shell rather than checking `available_skills` in an already-running session.
- OpenCode not detected for skill installation — only .claude and .agents roots are supported: Trap: OpenCode is detected in `src/lib/detect-agent.ts` via `OPENCODE_CLIENT` env var and `PROCESS_NAME_AGENTS` map — looks like it should drive skill installation. But detection is for telemetry only. `installAgentSkills()` and `src/commands/cli/uninstall.ts` hardcode `agentRoots = ['.claude', '.agents']` — OpenCode is never a skill install target. Fix: to add OpenCode skill support, add its root dir to `agentRoots` in both `agent-skills.ts` and `uninstall.ts`, and add a `detectOpenCode()` function parallel to `detectClaudeCode()`.
- OpenCode skill scan is session-scoped — new skill files installed mid-session are invisible until restart: Trap: `installAgentSkills()` writes skill files to `
/.claude/skills/sentry-cli/` and the files appear on disk immediately — looks like OpenCode will pick them up. But OpenCode scans skills once per session at startup via `InstanceState.make` and memoizes the result for the session lifetime — no hot-reload. Fix: any session that started before the skill directory was created will show the old count forever. Users must start a new OpenCode session after `sentry setup` installs skills. Confirmed: `init count=6` appeared in every log across 2 days because `/.claude/skills/sentry-cli/` didn't exist until Jun 25 19:38 UTC; `debug skill` immediately returned 7 in a fresh process.
- parseWithHash short-circuits before the main validateResourceId guard — must self-validate (CLI-1G1): GitHub-style `org/project#SHORTID` issue identifiers handled by `parseWithHash()` in `src/lib/arg-parsing.ts`, inserted in `parseIssueArg` AFTER the `@`-selector block and BEFORE the `validateResourceId(input.replace(/\//g,''))` guard (line ~1115, which rejects `#`). Because it runs before that guard, `parseWithHash` MUST validate BOTH the project prefix AND the fragment itself. `validateResourceId` permits `:`, so `:` mixed with `#` is rejected explicitly. Semantics: `org/project#ID` → delegates to `parseWithSlash('org/project/ID')`; `project#ID` → `project-search` via `parseProjectIdentifier`; `#ID` → bare identifier via `parseBareIssueIdentifier`. `parseProjectIdentifier` is shared with `parseWithColon`. BEHAVIORAL CHANGE: `CLI-G#anchor` went from `ValidationError` → `project-search{projectSlug:'cli-g', suffix:'ANCHOR'}`. Test at `arg-parsing.test.ts` injection-hardening block updated accordingly.
- pnpm nested script invocation loses TTY — inline tsx to fix: Trap: `"cli": "pnpm tsx src/bin.ts"` creates nested pnpm invocations (pnpm → /bin/sh → pnpm → /bin/sh → tsx → node). Each inner pnpm layer pipes stdio, so `process.stdin.isTTY` and `process.stdout.isTTY` are `undefined` in the final Node process. This breaks `sentry init` at three gates: `isNonInteractiveContext()` (init.ts:182), `isInteractiveTerminal()` (factory.ts:62), and the wizard preamble (wizard-runner.ts:376). Fix: inline tsx directly — `"cli": "tsx --import ./script/require-shim.mjs src/bin.ts"` and same for `dev`. Single-layer `pnpm run` uses `stdio: 'inherit'`; nested pnpm does not. Approach B (`node --import tsx/esm ...`) rejected as fragile (tsx internal API). Approach C (shell wrapper) rejected as non-portable. Keep the `tsx` alias for non-interactive scripts.
- pnpm test runs generate:docs + generate:sdk before vitest — too slow for direct invocation: Trap: `pnpm test` (or `pnpm run test:unit`) runs `generate:docs && generate:sdk` before vitest, making the full suite take minutes even for small changes — looks like a normal test command. Fix: invoke vitest directly to skip doc/SDK generation: `vitest run test/lib test/commands test/types`. This drops runtime from minutes to ~2-10s. Use `pnpm run test:unit` only when you need the generated artifacts to be fresh (e.g., testing doc generation itself).
- pnpm test triggers generate:docs + generate:sdk pre-steps — always times out in CI-like environments: Trap: `pnpm test` looks like the standard way to run tests. But `test:unit` = `pnpm run generate:docs && pnpm run generate:sdk && vitest run test/lib test/commands test/types --coverage` — the doc/SDK generation pre-steps cause 120s+ timeouts. Fix: run vitest directly on specific test files: `npx vitest run test/lib/dif test/commands/debug-files` (or similar scoped paths). Use `pnpm test` only for final pre-commit validation. `test:init-eval` is the only script without the preamble (uses `--testTimeout 600000` instead).
- prepareZipDifs null return means 'not a zip' OR 'container skipped' — non-null (even empty) means fully handled: Trap: `null` from `prepareZipDifs` previously meant only 'not a ZIP'. After the maxTotalSize refactor, `null` also means 'container skipped wholesale (too large / malformed)' — both cases should fall through to normal file handling. Fix: the contract is now 'non-null result (even empty array) means the path was a `.zip` and is fully handled; `null` means fall through.' The `continue` in `prepareDifs` fires on any non-null result, so an empty array correctly skips the file without treating it as a non-ZIP. Document this dual-null semantics in JSDoc on `prepareZipDifs`.
- prepareZipDifs oversizedCount must NOT feed exit-driving counter — format unknown pre-decompression: Trap: zip entry oversized warnings look like they should increment `oversizedCount` in `prepareDifs`, just like on-disk file oversize does — both are 'too large' signals. But a compressed entry's format is unknown until inflated, so it can't be attributed to the requested `--type`. Counting it would turn an unrelated oversized asset inside a `.zip` into a false 'all matched files too large' failure and wrong exit code. Fix: `prepareZipDifs` returns `PreparedDif[]` (not `{ prepared, oversizedCount }`); oversized zip entries warn per-entry inside `readZipDifEntries` only. The `oversizedCount` counter in `prepareDifs` is exclusively for format-accurate on-disk files.
- print-sources must never claim to preview bundle-sources while listing sources bundle-sources would skip: Trap: `print-sources` iterates ALL objects in an archive (full inspection value), while `bundle-sources` only bundles the first object with debug info (`selectBundledObject`). Listing all objects then showing a `bundle-sources` hint looks like a preview of that command — but it isn't. Fix: add multi-object warning naming the exact slice that would be bundled (via `selectBundledObject`), add `hasDebugInfo` to JSON output so consumers can apply the same selection rule, and ensure footer hint distinguishes no-objects / enumeration-failure / no-sources. 🔴 Directive: never claim to preview `bundle-sources` while collecting sources `bundle-sources` would never collect.
- ruzstd partial decompression: must validate output size explicitly: Trap: `ruzstd::StreamingDecoder` (unlike `zstd::bulk::decompress`) silently returns a partial result when passed a too-small `size` — it does NOT error. Fix: read `size + 1` bytes into the output buffer, then assert `decompressed.len() == size`; return `None` on mismatch. This matches `zstd::bulk::decompress` error-on-mismatch semantics. Confirmed via test: exact(560)→Some(560)✓, toosmall(550)→None✓, toolarge(570)→None✓.
- scan.ts difFromBuffer: calls embeddedPpdbDif for ALL formats — doubles WASM memory for non-PE files: Trap: `difFromBuffer()` (scan.ts:662) calls `embeddedPpdbDif()` unconditionally for every buffer regardless of format — ELF, Mach-O, Breakpad all trigger a full `new Archive(data)` + `objects()` + `asPe()` before returning `undefined`. Looks correct because `asPe()` returns `undefined` for non-PE. But it contradicts the peek-before-parse strategy and doubles peak WASM linear memory across the whole scan. Fix: gate `embeddedPpdbDif` on `format === 'pe'` by threading `peeked.format` (on-disk path, scan.ts:511) or `format` (zip path, scan.ts:722) into `difFromBuffer`; or cheap-gate inside `embeddedPpdbDif` via `peekFormat(header)` before full parse.
- scan.ts: oversized PE early-return must not skip embedded PPDB extraction: Trap: in `prepareFileDif` (src/lib/dif/scan.ts), gating on a PE's on-disk size vs `maxFileSize` looks like a safe early-exit optimization — but it isn't PPDB-aware. A large assembly can contain a small embedded .pdb well within the limit, yet the pre-fix code returned before `embeddedPpdbDif` ever ran, silently dropping the extractable PDB (Bugbot finding, PR #1163, bug 47). Fix: `embeddedPpdbDif` cheap-peeks only the first `PEEK_HEADER_BYTES` (4096) via `peekFormat()` to confirm PE format before doing a second full `Archive` parse, so oversized-container gating never blocks embedded-content extraction. The oversized signal instead threads end-to-end: `embeddedPpdbDif` → `difFromBuffer` → `readMatchedDif` → `prepareFileDif` → `prepareDifs` (`oversizedCount+=1`), and is only counted when `portablepdb` format is actually requested via `formatMatches`.
- sentry-cli SKILL.md has non-standard frontmatter fields (version, requires) — safe for OpenCode but worth knowing: SKILL.md frontmatter includes `version` and `requires: {bins: ["sentry"], auth: true}` — fields not in the OpenCode skill spec. OpenCode's `isSkillFrontmatter()` only validates `name: string` and optional `description: string`; extra fields are silently ignored. gray-matter (js-yaml) parses the nested `requires` object without error. Trap: nested objects in frontmatter look like they'd cause a YAML parse failure or schema rejection. They don't — confirmed via gray-matter test and zero "failed to load skill" log entries. The skill loads correctly; absence from a session is always a stale-snapshot issue, not a parse issue.
- Silent nonexistent path in scan — throw ValidationError instead of skip: Trap: `scanPaths` silently skipped nonexistent paths (ENOENT from `stat` → `log.debug` + continue). Users got empty results with no indication the path was missing. Fix: for explicitly provided paths (not directory children), throw `ValidationError` with the path name. Directory children that don't exist are still silently skipped.
- skill-eval E2E tests fail on Anthropic API network errors — not a code regression: Trap: `test/e2e/skill-eval.test.ts` failures (`claude-sonnet-4-6 meets threshold`, `claude-opus-4-6 meets threshold`) look like regressions introduced by the current PR. Root cause: these tests call `api.anthropic.com` directly — `[planner] API error: Invalid response body ... Premature close` is an external Anthropic API outage, not a code bug. All 126 non-LLM E2E tests pass. Fix: confirm by checking logs for `Premature close` pattern; if present, stop re-running (wastes CI resources) and post a PR comment documenting the outage. Do not merge while CI is red — wait for API recovery.
- SQLite transaction() ROLLBACK can throw, discarding original error: (gotcha) SQLite transaction ROLLBACK error-swallowing trap: In `src/lib/db/sqlite.ts`, `transaction()` catches errors and runs `this.db.exec('ROLLBACK')`. If ROLLBACK itself throws, the original error is lost. Fix: `const origErr = e; try { this.db.exec('ROLLBACK'); } catch (rbErr) { log.debug(...); } throw origErr;`
- streamDecompressToFile: never emit 'drain' on ENOSPC — race drain against error to avoid hang: Trap: `writer.write()` returning false normally means wait for 'drain' before continuing. But on ENOSPC/EIO, the error fires while the buffer is full — 'drain' never fires, causing a permanent hang. Fix: `streamDecompressToFile()` races 'drain' against 'error' listeners so whichever fires first unblocks the loop. This is a standing directive: 'never emit drain' when an I/O failure occurs while the buffer is full. Same pattern applies to `downloadStableToPath()` which uses `arrayBuffer()` + `writeFile()` (not streaming) as a fallback due to the Bun event-loop GC bug (oven-sh/bun#13237).
- strip fails on Node SEA binaries — must strip BEFORE fossilize injection: Strip debug symbols must happen BEFORE fossilize SEA injection. Trap: `strip --strip-unneeded` on a plain Node binary saves ~17 MiB and still runs — looks like it should work on the final SEA binary too. But after postject injects the SEA blob, `strip` fails: 'section .text can't be allocated in segment 2'. Fix: as of fossilize 0.7.0, stripping is built into fossilize itself — it strips the copied binary (already unsigned for macOS/Windows) BEFORE calling postject. Cross-strip from Linux to macOS silently fails (caught); native macOS runners strip correctly with `strip -x`. Windows skipped (no debug symbols). `stripCachedNodeBinaries()` was removed from `script/build.ts` in fossilize 0.7.0 update — fossilize handles it natively.
- symbolic-wasm: JS callback errors silently swallowed via .ok()? — must propagate as JsError: Trap: using `.ok()?` on `call1` (JS function invocation) and `dyn_into::<Uint8Array>()` looks like idiomatic Rust error-to-Option conversion. Fix: both failures must propagate as `JsError` to the caller — thrown JS exceptions yield partial bundles with no feedback; non-`Uint8Array` returns (ArrayBuffer, plain arrays) silently skip files. Pattern: capture callback error in `Option<JsValue>` outside closure, set it on failure, check after closure returns. Make `with_object` generic over `E: From<JsError>` so both error paths unify. Flagged by Cursor Bugbot (Medium) and Sentry Seer (Medium) on PR #991.
- Symlink cycle hang in recursive file collection — use lstat + visited-realpath set: Trap: `collectFiles` uses `stat` (follows symlinks) with no cycle detection. Directory symlinks pointing to ancestors cause unbounded recursion — never returns. macOS `.framework`/dSYM trees routinely contain cyclic symlinks. Fix: use `lstat`, skip symlinked directories, and track visited realpaths in a `Set` to break cycles. File symlinks are safe to follow.
- Upload assembly `not_found` after deadline is a real failure — must set exit code 1: Trap: upload assembly only treated `"error"` state as failure; `"not_found"` was treated as incomplete (exit 0 with debug log). But after deadline, `not_found` means chunks were never delivered — a genuine failure. Fix: treat `not_found` as failure with `log.warn` + exit code 1. Also upgrade deadline-break log from `debug` to `warn`. Discovered during self-review of `debug-files upload` PR.
- UPX destroys ELF notes — incompatible with Node SEA binaries: Trap: UPX compresses Node binaries from 99 MiB to 25 MiB and the compressed binary still runs — looks like a huge win. But UPX rewrites the entire ELF structure: original binary has 2 ELF notes (NT_GNU_BUILD_ID + NT_GNU_ABI_TAG), UPX'd binary has 0 notes and 0 sections. NODE_SEA_BLOB is stored as an ELF note — UPX destroys it. Fix: use `strip --strip-unneeded` instead, BUT only on the plain Node binary BEFORE fossilize SEA injection. After injection, `strip` fails with 'section .text can't be allocated in segment 2' — the SEA blob corrupts the ELF section-to-segment mapping. Strip the `.node-cache/` binaries before calling fossilize. Saves ~17 MB raw / ~4 MB compressed. Strip is idempotent — already-stripped binaries are unchanged. Recommended order: strip cached Node → fossilize (inject) → binpunch → gzip.
- useTestConfigDir afterEach: never delete CONFIG_DIR_ENV_VAR — always restore previous value: Trap: deleting `process.env.SENTRY_CONFIG_DIR` in `afterEach` looks like proper cleanup. But `preload.ts` always sets `SENTRY_CONFIG_DIR`, so `savedConfigDir` is always defined — deleting it causes subsequent test files' module-level code or `beforeEach` hooks to read `undefined`. Fix: always restore the previous value, never delete. The `else { delete process.env[CONFIG_DIR_ENV_VAR] }` branch is intentionally omitted in `test/helpers.ts` `useTestConfigDir`. Same principle applies in `test/fixture.ts` `setAuthToken()` finally block — the delete there is acceptable only because it's a scoped try/finally restore, not a test lifecycle hook.
- WASM handles (Archive/ObjectFile/PeFile) must be freed — new DIF functions multiply live handles: Trap: `extractEmbeddedPpdb` and `createIl2cppLineMapping` create `Archive`/`ObjectFile`/`PeFile` instances (all expose `free()`/`[Symbol.dispose]()`) but never release them. PRs #1163/#1164 multiply live handles per PE up to ~4 `Archive`s (`matchedDif`, `extractEmbeddedPpdb`, `createIl2cppLineMapping`, `createSourceBundle`). Looks safe because JS GC eventually collects — but WASM linear memory is not GC'd; handles accumulate until process exit. Fix: use `using archive = new Archive(...)` (explicit resource management) in all new functions that create WASM objects.
- wasm-pack test --node never catches js_name/js_class binding bugs like Object shadowing: Trap: `wasm-pack test --node` looks like a complete test of the WASM package — it runs Rust tests compiled to WASM. But it builds its own JS glue and never loads the `--target web` artifact. So `export class Object` shadowing the JS global `Object` passes all wasm-pack tests. Fix: use the two-layer approach — (1) `wasm_bindgen_test` + `wasm-pack test` for bulk behavior, (2) artifact smoke test that does `npm pack` → install into temp dir → `import "@sentry/symbolic"` → assert API loads. The smoke test catches packaging regressions that wasm-pack misses. Fix for Object shadowing: `#[wasm_bindgen(js_name = "ObjectFile")]` + `#[wasm_bindgen(js_class = "ObjectFile")]`; Rust struct name `Object` unchanged.
- wasm-pack test never loads the published package — builds its own glue instead: Trap: `wasm-pack test` looks like the right way to test the published `@sentry/symbolic` package (it builds WASM and runs tests). Fix: `wasm-pack test` builds its own glue (`--target web`, `symbolic.js`, `initSync`) and never loads what the package actually ships via `exports`/`files`. Testing the published package requires loading it through the actual package entry points, not wasm-pack's test harness. Confirmed by Burak Yigit Kaya on Jun 22 2026.
- Whole-buffer matchAll slower than split+test when aggregated over many files: Grep/scan traps in `src/lib/scan/`: (1) Whole-buffer `regex.exec` 12× faster per-file but ~1.6× SLOWER over 10k files — early-exit at `maxResults` via `mapFilesConcurrent.onResult` wins. (2) Literal prefilter is FILE-LEVEL gate (`indexOf`→skip); per-line verify breaks cross-newline patterns and Unicode length-changing `toLowerCase`. (3) Extractor `hasTopLevelAlternation`+`skipGroup` must call `skipCharacterClass`. (4) Wake-latch race: use latched `pendingWake` flag, not `let notify=null; await new Promise(r=>notify=r)`. (5) `mapFilesConcurrent` filters `null` but NOT `[]` — return `null` for no-op files. (6) `collectGlob`/`collectGrep` must NOT forward `maxResults` to iterator; drain uncapped, set `truncated=true`. Worker pool: lazy singleton, size `min(8, max(2, availableParallelism()))`. Matches encoded as `Uint32Array` quads transferred via `postMessage` (~40% faster). `new Worker(new URL(...))` HANGS in SEA binaries — use Blob+URL.createObjectURL. FIFO `pending` queue per worker. `ref()`/`unref()` idempotent — only unref when `inflight` drops to 0. Disable via `SENTRY_SCAN_DISABLE_WORKERS=1`.
- Windows rename() raises EPERM/EBUSY on open destination — atomic write is POSIX-only: Trap: `rename()` for atomic file swap looks cross-platform because Node.js exposes it on all OSes. But on Windows, if the destination file is open by a concurrent reader, `rename()` raises EPERM or EBUSY — the swap fails rather than being atomic. The JSDoc claim 'eliminates the truncation race' is unqualified but only holds on POSIX. `win32-x64` is a shipped sentry-cli target. Impact is graceful (write returns null, captureException fires) but the atomicity guarantee must be documented as POSIX-only. Fix: qualify JSDoc with 'on POSIX systems'; on Windows the fallback is non-atomic `writeFile` (same as before the patch).
- worktree node_modules symlink causes esbuild host/binary version mismatch: Trap: when running multiple agent worktrees in parallel, `node_modules` may be a symlink to a sibling worktree's install — looks fine until esbuild runs. Error: 'Host version X does not match binary version Y' crashes all generate scripts and typecheck. Fix: remove the symlink and run `pnpm install` in the worktree root to get an independent `node_modules`. The symlink is created by mutation-test workflows that intentionally share `node_modules` for speed — but only safe when both worktrees are on the same branch/lockfile.
- zstd-sys supports wasm32-unknown-unknown via wasm-shim — C zstd usable in WASM without libc: Trap: `zstd-sys` (C zstd) looks incompatible with `wasm32-unknown-unknown` because it compiles C code and the target has no libc/sysroot. Fix: `zstd-sys v2.0.16+` ships `wasm-shim/` headers that `#define` `malloc`/`free`/`memcpy` etc. to `rust_zstd_wasm_shim_*` Rust functions backed by Rust's allocator. `build.rs` auto-enables the shim for `wasm32-unknown-unknown` (controlled by `no_wasm_shim` feature). Requires clang/lld/llvm installed (`sudo apt-get install -y clang lld llvm` on Ubuntu). `ruzstd` was evaluated as pure-Rust alternative but rejected by maintainers (Dav1dde) as significantly slower. CI `wasm-build` job AND `build.yml` `npm-package` job (which runs `make npm`) BOTH must install clang/lld/llvm before building — `npm-package` job previously only had `binaryen` and `rustup wasm32-unknown-unknown`, causing silent C-zstd compile failures.
- @sentry/symbolic 13.4.0 API: Archive/ObjectFile class contract and WASM memory management: Exported classes: `Archive` (`objects()`, `peek()`, `fileFormat`, `objectCount`), `ObjectFile` (`debugId`, `codeId`, `arch`, `fileFormat`, `hasDebugInfo`, `hasSources`, `hasSymbols`, `hasUnwindInfo`, `kind`, `debugSession()`), `DebugSession` (`files()`, `sourceByPath()`), `SourceBundleWriter` (`writeObject()`, `collectIl2cppSources` setter, `isEmpty`), `SourceFileDescriptor` (`type`, `contents`, `url`, `path`, `sourceMappingUrl`, `debugId`). NOT exposed: `ObjectLineMapping::from_object` (il2cpp line-mapping DIF), BCSymbolMap parsing, UuidMapping plist parsing, embedded Portable PDB extraction. These gaps make BCSymbolMap/il2cpp DIF creation impossible without native tooling.
- @sentry/symbolic release flow: build from master → local tarball → npm publish via craft: Steps: 1. Checkout `origin/master` in `~/Code/getsentry/symbolic` — ensures all merged PRs included. 2. Run `cd symbolic-wasm && bash build-npm.sh` — compiles wasm32, runs wasm-opt, packs tarball, runs smoke test (3 assertions). 3. Pin CLI `package.json` to `file:` tarball for local validation — run tests, typecheck, lint. 4. Revert `file:` pin before committing — wait for npm publish (craft flow via getsentry/publish). 5. Once new version (e.g. 13.5.0) appears on npm, update `package.json` to semver pin. Gotchas: - Merge ≠ publish: PR #997 merged Jun 24 but npm still showed 13.4.0 — always verify `npm dist-tags` before committing semver pin. - `file:` tarball pin must never be committed to the CLI repo. Verify: - [ ] `npm view @sentry/symbolic dist-tags.latest` shows expected version. - [ ] Smoke test 3/3 pass including 'debug session enumerates referenced source files'.
- @sentry/symbolic: object.has_sources() only reports embedded sources — use debug_session.files() to detect any sources: In `@sentry/symbolic` (mirroring `print_sources.rs` in legacy Rust sentry-cli): `object.has_sources()` only reports *embedded* sources, NOT referenced files. To detect whether an object has any sources at all, use `debug_session.files().next().is_none()`. Core enumeration pattern: `Archive::parse(&data)` → `archive.objects()` → `object.debug_session()?.files()` → `FileEntry.abs_path_str()` → `debug_session.source_by_path(abs_path)` → `SourceFileDescriptor` (fields: `contents()`, `url()`, `debug_id()`, `source_mapping_url()`). PE with embedded PDB: also handle via `pe.embedded_ppdb()`.
- atomicWriteFile in agent-skills.ts: same-dir temp + rename guarantees no partial reads: `atomicWriteFile(destPath, content)` at `src/lib/agent-skills.ts:72`: writes to `.<name>.<pid>.<rand>.tmp` in the same directory as `destPath`, then calls `rename()` into place. Same-directory placement guarantees same filesystem → POSIX atomic rename. Concurrent readers never observe a truncated or partially-written file. Temp file is cleaned up on error. Used by `writeSkillFiles()` (replacing in-place `writeFile`). Skills are written on every version — write-if-changed optimization was explicitly rejected as unnecessary.
- build upload iOS normalization: XCArchive dir → zip, IPA → XCArchive zip: In `src/lib/build/index.ts`, iOS uploads are normalized before sending to the API. XCArchive directories are walked recursively (sorted, deterministic), each file stored as `<basename>.xcarchive/<relative-path>` in a zip. IPAs are re-wrapped as XCArchive: strip `Payload/` prefix, move app bundle under `archive.xcarchive/Products/Applications/`, generate a synthetic `Info.plist` with `ApplicationPath`. App name extracted from `Payload/<name>.app/` directory entry. `Assets.car` files are passed through verbatim — not parsed into per-asset images (by design). Both normalizers prepend `.sentry-cli-metadata.txt`. Symlinks and POSIX permissions are NOT preserved (zip has no cross-platform permission standard).
- bundle-sources exit-code convention: manual exitCode=1 vs OutputError for no-sources path: In `src/commands/debug-files/bundle-sources.ts`, the no-sources-found path uses `this.process.exitCode = 1` (not `OutputError`). This is a known Medium deviation from the framework-blessed pattern: `OutputError` (exit 60) is robust against framework finalization resetting exitCode; manual assignment is fragile. The `-o` flag also does NOT create parent directories (unlike `bundle-jvm`), so `writeFile` throws raw ENOENT if the parent doesn't exist. Both are accepted as-is in PR #1126 — do not 'fix' them without a follow-up PR discussion. Exit code map: ValidationError→21, success→0, no-sources→1.
- CI Node version pinning: centralized env block per workflow file, ternary for matrix jobs: Node CVE-2026-48931 fix: `NODE_VERSION_22="22.23.1"`, `NODE_VERSION_24="24.18.0"` (22.23.0 had the vulnerability; fix landed in 22.23.1 via nodejs/node#64004). Pattern: add top-level `env:` block to each workflow file (ci.yml, release.yml, sentry-release.yml, docs-preview.yml) with both constants + rationale comment. Reference via `${{ env.NODE_VERSION_22 }}`. Matrix jobs (build-npm) use ternary: `${{ matrix.node == '24' && env.NODE_VERSION_24 || env.NODE_VERSION_22 }}` — matrix labels stay as bare majors (`["22","24"]`) for job naming. Gotcha: `eval-skill-fork.yml` has no `setup-node` step at all — must add one explicitly [[019f03bb-f9cf-7208-a183-d4f0074480f9]].
- clack-utils.ts filename preserved intentionally — rename deferred to next cleanup PR: `src/lib/init/clack-utils.ts` filename kept (not renamed to `wizard-utils.ts`) to keep PR 4 diff focused on clack removal. No clack references remain in the file. `WizardCancelledError` lives here. `abortIfCancelled<T>()` return type uses `Exclude<T, symbol>` to narrow union types. `FEATURE_DISPLAY_ORDER` and `CANONICAL_STEP_ORDER` (12 steps) also defined here. Rename is intentionally deferred.
- createSourceBundle: object selection, sync provider contract, writer lifecycle: `createSourceBundle(data, objectName, readSource)` in `src/lib/dif/index.ts`: selects `objects.find(o => o.hasDebugInfo) ?? objects[0]`; returns `{bundle:null, debugId:null, fileCount:0}` if no objects. `SourceBundleWriter.writeObject` is synchronous — provider/filter callbacks must be sync (`readFileSync` in bundle-sources.ts). Writer is single-use: `writeObject` calls `__destroy_into_raw()` (zeroes ptr, unregisters FinalizationRegistry). Provider returning `null` signals skip (WASM glue checks `arg0 == null`). `bundle === null || fileCount === 0` correctly catches manifest-only ZIPs with zero source files.
- debug-files upload: DIF assemble wire format and chunk-upload pipeline: Native DIF assemble body: `{ [overallSha1]: { name, debug_id?, chunks: string[] } }` — identical shape to `proguard.ts`/`dart-symbols.ts`. `debug_id` is advisory (server re-parses). Per-file upload: each file chunked as raw bytes via `hashBuffer`; primary object selected via `selectBundledObject` (first with debug info, fallback to first). Assemble endpoint: `projects/${org}/${project}/files/difs/assemble/`. Constants: `DEFAULT_MAX_DIF_SIZE=2GB`, `DEFAULT_MAX_WAIT=300s`. `--wait` flag controls whether to poll until assembly completes. Deferred: ZIP scanning, BCSymbolMap/dsymutil, Xcode derived-data, il2cpp mapping (require native tools not available in WASM).
- debug-files/bundle-sources: SourceBundleWriter is one-shot/consuming — create fresh per call: `SourceBundleWriter.writeObject` in `@sentry/symbolic` consumes the writer via `__destroy_into_raw()` — a single instance cannot be reused. `createSourceBundle(data: Uint8Array)` in `src/lib/dif/index.ts` creates a fresh writer per call. Returns `SourceBundleResult | null` (null = empty bundle, i.e. `writeObject` returned `undefined`). Wasm boundary masks original JS error messages thrown inside provider callbacks — errors do propagate but message is not preserved; tests should use `.toThrow()` without message assertion for provider-error cases.
- Dedupe resolved entity IDs in batch operations before API call: Batch issue merge (`src/commands/issue/merge.ts`): (1) Dedupe by resolved numeric ID after `Promise.all(args.map(resolveIssue))` — users may pass same entity as `CLI-K9`, `my-org/CLI-K9`, or `123`. Throw `ValidationError` if `new Set(ids).size < 2`. (2) Reject `undefined` orgs in cross-org check — bare numeric IDs without DSN/config resolve with `org: undefined`. (3) Pass `--into` through `resolveIssue()`; compare by numeric `id`, not `shortId`. (4) Sentry bulk merge API picks canonical parent by event count — `--into` is preference only; warn when API's `parent` differs.
- error-reporting.ts silencing rules: which error types are silenced and why: Current `classifySilenced()` branches (post-PRs #1148–#1153): - `OutputError` → `'output_error'` (piped output closed early) - `ContextError` → `'context_missing'` (user omitted required value; never a CLI bug) - `AuthError` (any reason: `not_authenticated`, `expired`, `invalid`) → `'auth_expected'` (all auth reasons are user/env state after CLI-19 fix) - `ApiError` status >400 && <500 → `'api_user_error'` - `ApiError` + `isSearchQueryParseError()` → `'api_query_error'` (status 400 with 'Error parsing search query' detail) - `TypeError` + `isNetworkError()` → `'network_error'` (raw 'fetch failed' only) - else → `null` (captured) Metric emission must never block error handling — all metric/logger calls in `recordSilencedError()` are wrapped in try/catch.
- getsentry/symbolic CHANGELOG.md: use bold **Features** style, not ### headings: Danger bot on getsentry/symbolic PRs requires a CHANGELOG.md entry before merge. The bot's example uses `### Features` heading style, but the actual repo convention (established in PR #997 and #1004) uses `**Features**` bold style under the `## Unreleased` section. Always match the bold style. Entry format: `- WASM: expose \`Method()\` for <description>. ([#NNNN](url))`. Danger re-runs after the changelog commit and reports 'All green' when satisfied.
- getsentry/symbolic: follow-up PR pattern for merged PR review comments: getsentry/symbolic PR #988 (feat/source-bundle-provider) follow-up to Dav1dde review: **Implemented:** (1) `write_object_with_filter_and_provider` private inner method takes both filter `F` and provider `P: Fn(&str) -> Option<impl Read>`; public methods delegate to it. (2) `SharedCursor` (`Rc<RefCell<Cursor<Vec<u8>>>>`) removed — replaced with plain `Cursor::new(Vec::new())` + `into_inner()`. (3) Provider changed from destructive `contents.remove(path)` to non-destructive `contents.get(path).map(|v| v.as_slice())`. (4) Tests use `unwrap()` not `-> Result`. (5) `smoke-test.mjs` + `build-npm.sh` wiring + `ci.yml` `wasm-smoke` job added — closes gap where wasm bindings were only compile-checked on PRs. Byte-identity verified: sha256 `4d29224558f27174fef90e81d5c7e80fd388249f25cb48cfc4c5eb316e3b067b` identical BASE vs HEAD.
- getsentry/symbolic: Rust test conventions — unwrap() not -> Result: Per Dav1dde review on PR #988: test functions in `symbolic-debuginfo/tests/` must NOT return `Result` — use `unwrap()` so stack traces point to the assert location. If a shared error type is needed, define a module-level type alias: `type Result<T, E = Box<dyn std::error::Error>> = std::result::Result<T, E>;` but still prefer `unwrap()` in test bodies.
- getsentry/symbolic: wasm smoke test pattern — smoke-test.mjs + build-npm.sh + ci.yml wasm-smoke job: symbolic-wasm smoke test pattern: Two-file approach in `symbolic-wasm/npm/`: `smoke-test.mjs` (orchestrator: packs tgz, installs to temp dir, resolves wasm via exports map, spawns `node --test` on `package-smoke.test.mjs`) + `package-smoke.test.mjs` (node:test assertions against installed package via `initSync`). Test files excluded from `files[]` in `package.json` — nothing extra ships to consumers. Wired into `build-npm.sh` replacing bare `npm pack`. CI: `wasm-smoke` job in `ci.yml`. `cd symbolic-wasm/npm && npm test` runs the suite. Pack+install approach catches exports-map/resolution breakage, not just runtime errors. CRITICAL: `wasm-pack test --node` does NOT exercise the shipped artifact — it compiles tests with its own generated glue, never loads `--target web` `symbolic.js` + `symbolic_bg.wasm` via `initSync`. Confirmed by Burak Yigit Kaya: 'wasm-pack test sails past it because it builds its own glue and never loads what we ship'. PR #993 adds smoke tests.
- Grouped widget --limit auto-default via applyGroupLimitAutoDefault helper: Dashboard widget flag normalization: (1) Dataset aliases (errors→error-events) normalize ONCE at top of `func()` via `normalizeDataset()` in `src/commands/dashboard/resolve.ts`. In `edit.ts`, pass `normalizedFlags` to `buildReplacement` — `validateAggregateNames` reads `flags.dataset` and rejects valid aggregates like `failure_rate` if it sees raw alias. (2) Grouped widgets need `limit` (API rejects). `applyGroupLimitAutoDefault` defaults to `DEFAULT_GROUP_BY_LIMIT=5` only when user passed `--group-by` without `--limit`; skip for auto-defaulted columns like `["issue"]`. (3) Tests asserting `--limit` >10 survives into PUT body must use `display: "line"` — `prepareWidgetQueries` clamps bar/table to max=10.
- idle.ts eviction: upstream uses per-function cleanup in idle.ts, not centralized evictSession in pipeline.ts: Upstream (main branch) puts session eviction logic directly in `idle.ts` rather than a centralized `evictSession()` in `pipeline.ts`. `idle.ts` imports cleanup functions individually: `evictSession as evictGradientSession` from `@loreai/core`; also `deleteSessionAuth`, `clearAuthStale` from `./auth`; `deleteSessionCosts` from `./cost-tracker`; `deleteBillingPrefix` from `./cch`; `clearWarmupAuthDisabled` from `./cache-warmer`. The `startIdleScheduler` signature uses `onEvict?: (sessionID: string) => void` (upstream) vs `onEvictSession?: (sessionID: string) => boolean` (branch). Upstream inline `onEvict` in `pipeline.ts` cleans 5 Maps: `headerSessionIndex`, `ltmSessionCache`, `ltmPinnedText`, `stableLtmCache`, `cwdWarned`. When merging, adopt upstream's per-function approach and add any missing cleanup calls.
- Node version pinning convention: workflow-level env vars NODE_VERSION_22 / NODE_VERSION_24: As of PR #1145, all GitHub Actions workflows in sentry-cli (TypeScript) centralize Node version pins as workflow-level `env` vars: `NODE_VERSION_22: "22.23.1"` and `NODE_VERSION_24: "24.18.0"`. All `actions/setup-node` steps reference `${{ env.NODE_VERSION_22 }}` or `${{ env.NODE_VERSION_24 }}` — no bare `"22"`/`"24"` strings. Matrix jobs use ternary: `${{ matrix.node == '24' && env.NODE_VERSION_24 || env.NODE_VERSION_22 }}`. Motivation: Node 24.17.0/22.23.0 shipped `ERR_STREAM_PREMATURE_CLOSE` regression (CVE-2026-48931 http.Agent fix); fixed in 24.18.0/22.23.1 (nodejs/node#64004). When bumping Node, update the `env` block in each workflow file.
- parseIssueArg multi-line handling: first non-blank line wins (CLI-1G1): Issue identifiers are always single-line atomic tokens. `parseIssueArg` in `src/lib/arg-parsing.ts` uses `LINE_SPLIT_PATTERN = /\r?\n/` to split input, trims each line, and takes the first non-empty line (PR #1148, CLI-1G1). Rationale: `.trim()` only strips leading/trailing whitespace — embedded newlines from command substitution or agent note-appending survive and reach `validateResourceId`. Joining lines (like the `api.ts` LINE_BREAK_PATTERN approach) is wrong for atomic tokens because `CLI-ABC\n<note>` would produce garbage. Splitting on `\n` is safe because newlines are control characters already rejected by `validateResourceId` — splitting never breaks project display names with spaces.
- Preserve ApiError type so classifySilenced can silence 4xx errors: Preserve ApiError type for classifySilenced: `classifySilenced` (src/lib/error-reporting.ts) only silences `ApiError` with status 401-499 — wrapping in generic `CliError` loses `status` and causes 403s to be captured. Re-throw via `new ApiError(msg, error.status, error.detail, error.endpoint)` with terse message (`ApiError.format()` appends detail/endpoint). `ValidationError` without `field` collapses unfielded errors into one fingerprint; always pass `field`. Fingerprint rule changes don't retroactively re-fingerprint — manually merge new groups into canonical old parents. `ApiError` rule keys by `api_status + command`.
- scan.ts: per-object extraction errors always swallowed — never abort surrounding upload: In `src/lib/dif/scan.ts` and `src/lib/dif/index.ts`, extraction errors for embedded PPDBs (`extractEmbeddedPpdb`), IL2CPP mappings (`createIl2cppLineMapping`), and source bundles are caught per-object, logged at debug level, and swallowed — they never abort the surrounding upload or scan. This mirrors legacy Rust sentry-cli behavior. Similarly, `PeekResult.format` is never `'unknown'` — unrecognized formats return `null` from `peekHeader`. Nested ZIP archives are never recursed regardless of `scanZips` setting.
- Security dep bump pattern: pnpm.overrides for transitive vulns + direct version bumps: Security dep bump pattern for getsentry/cli: (1) Bump direct deps in `package.json`. (2) Add `pnpm.overrides` for transitive-only vulns (e.g. `shell-quote`, `qs`). (3) Add `pnpm.overrides` to `docs/package.json` for docs-workspace transitive vulns. (4) Run `pnpm install` in root, then in `docs/`. (5) After lockfile regen, verify no vulnerable versions remain and no previously-patched versions were downgraded — run `pnpm update <pkg>` to fix regressions. (6) Run `check:deps` to confirm no runtime deps added. Pre-existing WARNs about `@sentry/core`/`@sentry/node-core` patches on `@sentry/node`'s nested copies are expected. Trap: `pnpm install` can downgrade already-patched transitive deps (e.g. `hono` downgraded reintroducing GHSA vuln) — always verify versions post-regen.
- selectBundledObject: shared generic helper for first-debug-info-else-first selection: `selectBundledObject<T>(items: T[], hasDI: (t: T) => boolean): T | undefined` in `src/lib/dif/index.ts` is the single source of truth for 'first object with debug info, fallback to first object' selection. Used by both `createSourceBundle` (WASM `Object[]`) and `print-sources` (multi-object warning). Chosen over duplicating the heuristic in each consumer — divergence between bundler and inspector is structurally impossible. Generic predicate parameter lets it work with both WASM `ObjectFile` and `DifObjectSources` arrays.
- sensitive argv flags must never reach telemetry — redactArgv() in cli.ts: `SENSITIVE_ARGV_FLAGS = new Set(['token', 'auth-token'])` in `src/cli.ts`. `redactArgv()` replaces values of these flags with `[REDACTED]` before any telemetry call. This is an absolute invariant — never pass raw `process.argv` to telemetry without running through `redactArgv()` first.
- Sentry SDK tree-shaking patches must be regenerated via bun patch workflow: Sentry SDK tree-shaking via bun patch: `patchedDependencies` in `package.json` strips unused exports from `@sentry/core` and `@sentry/node-core`. Non-light root of `@sentry/node-core` pulls uninstalled `@opentelemetry/instrumentation` — **always import from `@sentry/node-core/light`** (subpaths: `.`, `./light`, `./light/otlp`, `./init`, `./loader`, `./import`). No supported import for `HttpsProxyAgent`. Bumping SDK: remove old patches, `rm -rf ~/.bun/install/cache/@sentry`, `bun install`, `bun patch @sentry/core`, edit, `bun patch --commit`; repeat for node-core. Preserved: `_INTERNAL_safeUnref`, `_INTERNAL_safeDateNow`, `nodeRuntimeMetricsIntegration`. Before stripping any core export, grep `node-core/build/{cjs,esm}/light/sdk.js` for runtime usage (e.g. `spanStreamingIntegration` when `traceLifecycle === 'stream'`). Remove `.bun-tag-*` hunks from generated patches. Manual `git diff` patches fail.
- sentry-cli banner local test commands (isTTY-gated): Banner only renders in interactive terminals (`if (process.stdout.isTTY)`). Test commands: `pnpm cli help` or `pnpm cli` (fastest, tsx via src/bin.ts, no build); `pnpm cli init` (Ink wizard banner path). Quick sanity check without CLI: `FORCE_COLOR=3 pnpm exec tsx -e "import('./src/lib/banner.ts').then(m => console.log(m.formatBanner()))"`. Env vars: `FORCE_COLOR=3` forces color output; `NO_COLOR` or `SENTRY_PLAIN_OUTPUT` strips gradient.
- setup.ts bestEffort() wrapper: post-install steps must never crash setup: `src/commands/cli/setup.ts` `bestEffort(stepName, fn)` wraps non-essential post-install steps (recording install info, shell completions, agent skills) in try/catch. On failure: calls `warn(stepName, error)` + `captureException(error, { level: 'warning', tags: { 'setup.step': stepName } })`. These steps must NEVER crash setup — enforced by `bestEffort()`. `runConfigurationSteps()` applies `bestEffort()` independently to all 4 steps. Install dir priority: (1) `$SENTRY_INSTALL_DIR`, (2) `
/.local/bin` if exists+in PATH, (3) `/bin` if exists+in PATH, (4) `~/.sentry/bin` fallback. Welcome message only on fresh install (not upgrades).
- Shared pagination infrastructure: buildPaginationContextKey and parseCursorFlag: Pagination infrastructure + org flag injection: Bidirectional pagination via cursor stack in `src/lib/db/pagination.ts`. `resolveCursor(flag, key, contextKey)` maps keywords (next/prev/first/last) to `{cursor, direction}`. `advancePaginationState` manages stack — back-then-forward truncates stale entries. Critical: `resolveCursor()` must be called INSIDE `org-all` override closures, not before `dispatchOrgScopedList`. `issue list --limit` is global total: `fetchWithBudget` Phase 1 divides evenly, Phase 2 redistributes surplus. `trimWithProjectGuarantee` ensures ≥1 issue per project. Compound cursor (pipe-separated) enables `-c last` for multi-target pagination. JSON output wraps in `{ data, hasMore }` with optional `errors` array. `sort` flag is resolved once in `func()` before dispatch — never re-derived by infra. `handleOrgAllIssues` returns server order (no client-side sort). `isMultiProject` guard gates client-side sort at list.ts:1144-1148.
- symbolic-il2cpp integration tests: use symbolic-testutils dev-dependency with Object::parse pattern: Integration tests for `symbolic-il2cpp` live in `symbolic-il2cpp/tests/` (separate from unit tests in `src/`). Add `symbolic-testutils = { path = "../symbolic-testutils" }` as dev-dependency (path-only, safe for publishing — matches `symbolic-debuginfo` pattern). Use `ByteView::open(fixture("..."))` → `Object::parse(&view)?` to get a real `ObjectLike`. Fixture files live in `symbolic-testutils/fixtures/`. Native unit test with mock `ObjectLike` rejected as too heavyweight (many methods to implement). PR #1005 added `from_object_with_provider_empty_without_sources` and `from_object_with_provider_parses_source_info` tests.
- Telemetry instrumentation pattern: withTracingSpan + captureException for handled errors: For graceful-fallback operations, use `withTracingSpan` from `src/lib/telemetry.ts` for child spans and `captureException` from `@sentry/bun` (named import — Biome forbids namespace imports) with `level: 'warning'` for non-fatal errors. `withTracingSpan` uses `onlyIfParent: true` — no-op without active transaction. User-visible fallbacks use `log.warn()` not `log.debug()`. Several commands bypass telemetry by importing `buildCommand` from `@stricli/core` directly instead of `../../lib/command.js` (trace/list, trace/view, log/view, api.ts, help.ts).
- Testing Stricli command func() bodies via spyOn mocking: Testing Stricli command func() bodies: (1) `const func = await cmd.loader(); func.call(mockContext, flags, ...args)` with mock `stdout`, `stderr`, `cwd`, `setContext`. `loader()` return type union causes `.call()` LSP false-positives that pass `tsc --noEmit`. (2) When API functions are renamed, update both spy target AND mock return shape. (3) `normalizeSlug` replaces `_`→`-` but does NOT lowercase. (4) Bun `mockFetch()` replaces `globalThis.fetch` — use one unified mock dispatching by URL. (5) `mock.module()` pollutes module registry for ALL subsequent files — put in `test/isolated/` and run via `test:isolated`. (6) For `Bun.spawn`, use direct property assignment in `beforeEach`/`afterEach`.
- wizard-runner.ts: large shared context via initialState, not inputData — D1 row size limit: In `wizard-runner.ts`, large shared context (`dirListing`, `fileCache`, `existingSentry`) travels via `initialState` (not `inputData`) to avoid D1 per-row size overflow (see getsentry/cli-init-api#98). `MAX_RESUME_RETRIES = 3`, `RETRY_BACKOFF_MS = [2000, 4000, 8000]`. `resumeWithRetry()` handles stale-step recovery via `tryRecoverCurrentRunState()` when `isStepAlreadyAdvancedError()` detects 'was not suspended' 500.
- Always add new check scripts to both package.json and CI workflow: When introducing a new check or validation script, the user expects it to be registered in two places simultaneously: (1) as a named script in `package.json` alongside other `check:*` scripts, and (2) as a `- run: pnpm run <script-name>` step in `.github/workflows/ci.yml`. Never add to only one location. This applies to any new linting, validation, or verification script added to the project.
- Always check CI/PR status after opening or updating a pull request: After creating or pushing changes to a pull request, the user consistently checks the CI check status (e.g., via 'gh pr checks' or similar tooling) to monitor pass/fail/pending state of automated checks such as lint, typecheck, unit tests, security scans, and bot code reviews (Cursor Bugbot, Seer Code Review). When assisting, proactively run or offer to run PR status checks after PR creation/updates, summarize pass/pending/fail counts, and flag any pending or failing checks that need attention before considering the PR ready to merge.
- Always clarify that the repo uses plain git (not jj) when jj commands fail: When a jj command fails with 'no jj repo in .', the user consistently clarifies that the repo is a plain git repo and that jj's 'never fails on conflict' behavior is being referenced conceptually — meaning conflicts should be recorded/resolved rather than aborting operations. The agent should: (1) fall back to git commands immediately without retrying jj, (2) handle merge conflicts by stashing, pulling, and resolving (e.g., `git checkout --theirs` for files like `.lore.md`), and (3) not attempt `jj git init` or any jj initialization. This pattern appears at the start of every build session.
- Always compare PR branch against main before reviewing changes: When reviewing a PR, the user consistently wants to understand exactly what changed in the PR branch versus main before diving into the content. This means fetching the remote branch if not available locally, running `git log main..origin/<branch>` to see commits, and `git diff` (with stat) to understand the scope of changes. The user explicitly frames this as needing to know 'what changes were made vs what actually exists on main.' Always establish this baseline diff context first before analyzing or discussing PR content.
- Always conduct thorough PR reviews with severity-classified findings: PR review standards: (1) Compare branch vs main first (`git log main..origin/<branch>`, `git diff --stat`). (2) Verify every PR description claim against actual source files at specific line numbers — never trust PR metadata. (3) Classify findings as BLOCKING vs NON-BLOCKING with file paths and line numbers. (4) Flag LLM-generated planning artifacts (e.g., DOCS-AUDIT.md) as blocking violations of repo conventions. (5) Investigate root causes — check bundle output, trace esbuild variable renaming, identify silent regressions. (6) Run relevant check scripts and grep codebase directly rather than reasoning from PR metadata.
- Always create a dedicated branch when updating fossilize versions: When a new version of fossilize is released, always create a branch named `chore/fossilize-{version}` tracking origin/main, update the dependency, remove any functionality now handled natively by fossilize (e.g., `stripCachedNodeBinaries()` removed in 0.7.0), verify the build succeeds, then commit with `chore: update fossilize to X.Y.Z`. Follow this exact pattern: branch → update dep → remove superseded code → build verify → commit → PR.
- Always explore e2e test infrastructure thoroughly before debugging or modifying tests: When approaching e2e test work, always explore the full infrastructure before making changes: `test/e2e/` (14 files: api, auth, bundle, completion, delta-upgrade, event, issue, library, log, multiregion, project, skill-eval, telemetry-exit, trace), `test/fixture.ts` (getCliCommand, runCli, createE2EContext), `test/helpers.ts` (useTestConfigDir, useEnvSandbox, resetHostScopingState, mintSntrysToken, extractFetchUrl), `test/mocks/` (server.ts, routes.js, multiregion.ts), `src/bin.ts`, `src/cli.ts`. Key: `getCliCommand()` returns `[SENTRY_CLI_BINARY]` if set, else `[process.execPath, 'run', 'src/bin.ts']`. `createE2EContext.run()` sets `SENTRY_AUTH_TOKEN: ''`, `SENTRY_TOKEN: ''`, `SENTRY_CLI_NO_TELEMETRY: '1'`. `test:e2e` runs without `--isolate --parallel`. Map full infrastructure before proposing fixes.
- Always flag import framework mismatches as blocking CI issues in PR reviews: When reviewing PRs, the user consistently identifies test files using the wrong import framework (e.g., `bun:test` instead of `vitest`) as a BLOCKING issue, not a non-blocking suggestion. This applies when the project has migrated frameworks and all other test files use the new one. The user expects the reviewer/assistant to explicitly label it as blocking (B1, B2, etc.) and distinguish it from non-blocking issues (N1, N2, etc.), using a clear severity classification system in PR review feedback.
- Always investigate root cause by tracing through multiple specific code layers before accepting a fix: When facing a runtime bug (especially undefined values from framework internals), the user consistently demands thorough investigation across multiple layers — framework source code (node_modules), wrapper utilities, bundler config, and call sites — before accepting any fix. The user explicitly rejects surface-level explanations and pushes for tracing the exact code path that produces the unexpected value. Only after exhausting the investigation does the user accept a defensive fix strategy. When directing investigation, the user specifies concrete areas to search (e.g., 4 specific code locations). Always read and analyze the relevant framework internals, not just application code.
- Always migrate Bun-specific APIs and tooling to Node.js equivalents: 🔴 Directive (repeated 25+ sessions): ALWAYS prefer editing existing files in the codebase. NEVER write new files unless explicitly required. NEVER proactively create documentation files (*.md) or README files — only create documentation files if explicitly requested by the user.
- Always monitor PR CI checks until fully green before proceeding: After opening or updating a PR, the user consistently wants the assistant to poll and report GitHub Actions CI status repeatedly (build jobs, unit tests, E2E tests, lint, security scans, etc.) until all checks pass. When a check is pending, keep polling rather than stopping early. When a check fails, investigate the specific failing job/step (fetch run ID, job ID, logs) to determine if it's a genuine issue or flaky test before deciding next steps. Only consider the PR ready (e.g., for merge) once mergeStateStatus is CLEAN and all required checks are pass/skipping. Report status updates with concrete details: check names, pass/fail/pending state, durations, job IDs, and run URLs.
- Always perform thorough codebase exploration before designing or implementing fixes: When investigating a bug or feature, the user consistently requests comprehensive upfront exploration across multiple files before any code changes. This includes: reading relevant command and API files completely, searching for all references to key terms/parameters, checking type definitions in SDK/node_modules, and understanding the full data flow from flags to API calls. The user expects the assistant to map out the entire call chain, identify misleading comments, and surface all related code paths before proposing a solution. Do not jump to fixes — first read all relevant files thoroughly and report findings.
- Always perform thorough quality reviews of PRs distinguishing blocking vs non-blocking issues: When reviewing PRs, the user expects a structured quality review that: (1) categorizes issues as BLOCKING vs non-blocking/low-priority, (2) verifies each claimed change against the actual codebase, (3) flags LLM-generated planning artifacts (e.g., DOCS-AUDIT.md) that violate repo conventions as blocking issues, (4) checks for missed/inconsistent changes across all affected files, and (5) confirms correct changes are working as intended. The user wants specific file paths and line numbers cited for each issue. Non-blocking issues should still be reported but clearly distinguished from blockers.
- Always plan systemic fixes with structured multi-problem breakdowns before implementation: When the user identifies documentation or tooling issues, they consistently organize them as numbered problems with precise file locations, line numbers, and root causes before any code is written. They expect the assistant to engage at the planning level first — proposing detection strategies, fix approaches, and tradeoffs — and to consolidate related problems (e.g., merging overlapping tasks) rather than treating each in isolation. Plans are written to files and iterated on. Implementation only follows after the plan is agreed upon. The user prefers systemic/automated fixes (e.g., derive patterns from package.json) over one-off patches.
- Always prefer systemic/automated solutions over one-off fixes: When the user identifies errors, gaps, or problems, they explicitly direct the assistant to create or fix systems that prevent the entire class of errors in the future, rather than applying isolated one-off fixes. This applies especially when evaluating code quality, reviewing PRs, or addressing bugs. The user wants automated checks (e.g., CI steps, lint rules, scripts) and general solutions that scale, not patches that only address the immediate symptom. When planning or executing fixes, always ask: 'Can this be automated or systematized?' and prefer that approach.
- Always read and document full file details before proceeding with analysis or implementation: When exploring a codebase, the user consistently reads files in full and records comprehensive structured details: exact line counts, all imports, every exported type/interface with their fields, all constants, all function signatures with their logic, and any notable comments or assertions. This applies to both source files and build/tooling scripts. The user expects the assistant to capture and reference these details precisely rather than summarizing loosely. When examining related files (e.g., a module and its consumers), the user reads each completely before drawing conclusions. This pattern applies during architecture exploration, feature planning, and documentation generation tasks.
- Always read source files thoroughly before asking questions or making changes: The user consistently reads full source files (often 400-900+ lines) and traces complete data flow pipelines across multiple modules before taking action. They examine types, constants, function signatures, and cross-module dependencies in depth. They do not ask clarifying questions upfront — instead they investigate the codebase themselves to build a complete mental model. When helping this user, proactively read all relevant files in full, trace imports and data flows end-to-end, and present comprehensive findings rather than asking what they want to know. Assume they want the full picture, not a summary.
- Always reference external tools and prior art when exploring build/size optimization approaches: When investigating build pipeline improvements or binary size reduction, the user consistently references specific external tools, repos, and contacts (e.g., Vercel's build-binary.mjs, binpunch, fossilize, Melkey's work) as starting points for evaluation. They expect the assistant to analyze whether each referenced approach actually applies to their specific setup before recommending it. The user wants a clear breakdown of what's relevant vs. irrelevant given their actual architecture (e.g., 'we already use esbuild full bundling, so node_modules stripping doesn't apply'), followed by concrete alternative opportunities ranked by impact.
- Always research technical approaches thoroughly before implementation: When facing a significant technical decision or migration, the user consistently requests deep research into multiple approaches before writing any code. This includes: fetching specific upstream documentation/source files (e.g., BUILDING.md, configure.py), identifying concrete flags/options, estimating build times, and evaluating cross-compilation feasibility. The user wants tradeoffs between paths laid out explicitly. Only after research is complete does implementation begin. When presenting research, include specific flags, URLs, estimated costs (time/size), and platform constraints.
- Always stage all modified files before committing, not just already-staged ones: When preparing to commit, the user reviews git status and expects ALL modified files to be staged together — not just files already in the index. If unstaged modified files exist alongside staged ones, the user treats this as an incomplete commit state that needs to be resolved before proceeding. The user reviews the full list of changed files (staged + unstaged) as a checklist against completed tasks, and expects the commit to encompass all related changes from the session as a single coherent unit.
- Always store plans as markdown files in the `.opencode/plans` directory with timestamp-prefixed filenames: When working in plan mode, the user expects plans to be written to `.opencode/plans/` as markdown files. Filenames follow the pattern `{timestamp}-{slug}.md` (e.g., `1779289703678-sentryclirc-migration.md`). Some plans use descriptive slugs without timestamps (e.g., `require-conventional-pr-title.md`). Plans are created before implementation begins, and the assistant should call `plan_exit` when done planning. Plans may be edited iteratively during the planning phase before switching to build mode.
- Always switch from plan mode to build mode before executing changes: The user consistently uses a two-phase workflow: first planning (read-only exploration, writing a plan file), then explicitly approving a switch to build/agent mode before any changes are executed. When the user approves the mode switch, the assistant should immediately begin executing the existing plan file — typically by re-reading the key files to be modified. Never execute changes while still in plan mode, even if the plan is complete and approved. Wait for the explicit mode-switch approval before acting.
- Always track migration progress with explicit completion criteria and remaining blockers: The Bun→Node migration is complete only when `Bun.build({ compile: true })` is replaced by fossilize in `script/build.ts`. As of the current session, `script/build.ts` already uses fossilize (`--no-bundle`, `--out-dir dist-bin`, `--node-version lts`) with esbuild for bundling — the migration is complete. NODE_VERSION='lts' in build.ts. The user expects the assistant to track this state across sessions and confirm the migration is done. When resuming sessions, verify `script/build.ts` does not contain `Bun.build({ compile: true })` before declaring migration complete.
- Always track pre-existing failures separately from introduced regressions: When running tests, the user consistently distinguishes between failures that existed before their changes and failures caused by their changes. They verify pre-existing failures by checking out main/stashing changes and confirming the same failures reproduce. Only new failures introduced by the current branch are treated as actionable. When reporting test results, always clarify which failures are pre-existing (with evidence) versus newly introduced, and never treat pre-existing failures as blockers for the current fix.
- Always verify all tasks are complete before committing, then commit with descriptive conventional commit messages: Before committing, the user reviews a task checklist to confirm all items are completed or in-progress. They stage all relevant files, then commit with a conventional commit message (e.g., 'docs: fix stale Bun references and add systemic doc checks') that summarizes the scope of changes. The commit message reflects the primary theme of the work session. The user expects the assistant to help verify task completion status, check git status, and confirm the commit succeeds with a summary of files changed and insertions/deletions.
- Always verify code claims against actual file contents before accepting them as true: When evaluating PRs, documentation, or assertions about code behavior, the user systematically cross-checks every claim against the actual source files at specific line numbers. They expect the assistant to read the real files, confirm exact line locations, quote the relevant code/comments, and flag discrepancies between what is claimed and what the code actually does. The user marks confirmed findings with 🟡 (verified) and actionable directives with 🔴 (user assertion/directive). Never accept a PR description or assertion at face value — always ground-truth it against the codebase with precise line references.
- Always verify PR claims against actual codebase before accepting changes: When reviewing a PR, the user consistently directs the assistant to check each stated claim against the real source files on the main branch rather than trusting the PR description or commit messages. This applies especially to documentation PRs: the user wants specific file paths, line numbers, and code excerpts cited as evidence. The user also cross-checks automated tooling (scripts, CI configs) against what they actually produce. When a PR introduces fixes, the user wants confirmation that the underlying problem genuinely existed and that the fix is correct — not just that the PR author says so. Always run the relevant check scripts and grep the codebase directly rather than reasoning from PR metadata alone.
- Always work around the worktree conflict error when merging to main: When merging PRs locally, the user consistently encounters `fatal: 'main' is already used by worktree at ...` and expects a workaround to be applied automatically rather than treating it as a blocking error. The merge is always completed successfully despite this error (e.g., using `gh pr merge` via CLI or other workaround). Never stop or report failure when this specific worktree conflict appears — proceed with the merge using an alternative method and confirm the PR was merged successfully.
- Always work from a structured plan file before executing multi-step tasks: When tackling multi-step or multi-file changes, the user consistently creates a formal plan file (e.g., `.opencode/plans/<id>-<name>.md`) during a planning phase before any edits are made. The plan enumerates discrete numbered tasks with priorities and target files. Execution only begins after the user explicitly approves the plan. During execution, tasks are marked in_progress and completed sequentially. The user expects this plan-then-execute workflow to be followed strictly — no file edits during planning, and tasks tracked against the approved plan.
- Designer Steven Lewis directs Dammit Sans for headings, Rubik for body: Steven Lewis is the designer for the sentry-cli docs site. His direction: use Dammit Sans for headers, Rubik for body. Dammit Sans will eventually be scoped mainly to larger headings (h1 + some h2s). Dammit Sans missing glyphs: `/ * \ | ^ _ \`` plus `#` and `~` — fall back to Rubik Variable.
- Enforce internal wrapper conventions over direct Stricli imports: When implementing or reviewing CLI commands in this codebase, always use the project's internal wrapper modules instead of importing directly from '@stricli/core'. Specifically: import `buildRouteMap` from `../../lib/route-map.js` (which auto-injects standard aliases: list→ls, view→show, delete→remove/rm, create→new — do not add these manually), and use `buildCommand` from `../../lib/command.js` for all commands (which injects hidden flags like --log-level, --verbose, --org, --project, --fields, and handles auth/skipRcUrlCheck logic). When adding a new route/command, always update `src/app.ts` in two places: the alphabetical import block and the top-level `routes` object (plus `hideRoute` only if needed). Before implementing new features, research existing similar commands/patterns in the codebase (e.g., mirror `build upload` for `build download`, reuse streaming logic from `upgrade.ts`) rather than writing from scratch.
- Enforce read-only, verification-first workflow for code reviews and asset/config work in getsentry/cli: When the user requests a code review, PR audit, or investigation in the getsentry/cli repo (or similar), they consistently mandate a strict read-only discipline: never edit, write, or 'fix' files during review; record sha256 checksums (or diffs) of all files that will be touched before running any commands; restore files byte-identically if anything gets mutated; and confirm git status is clean afterward. They also expect rigorous, adversarial verification of concrete claims (e.g., exact character/column widths, banner never wraps, PR description matches actual diff) rather than taking descriptions at face value. When exploring history (PR commits, branch state, prior related PRs/issues), the user wants tool-based evidence (commit SHAs, diffs, metadata) cited rather than assumptions. Apply this pattern whenever asked to review, audit, or verify code/PRs/assets: default to read-only, checksum before/after, and independently verify specific technical assertions.
- Enforce strict read-only adversarial code review protocol for PR reviews: When asked to review a PR or commit in the getsentry/cli repo, treat it as a critical/adversarial, READ-ONLY review: never modify, write, or fix source files. Before running any commands, record checksums (sha256sum) of files expected to change; if any mutation occurs, restore files byte-identically and confirm `git status --short` is clean afterward. Builds/tests are allowed only if output is isolated (e.g., to dist/) and don't touch tracked source. Verify claims empirically rather than trusting the PR description — actually measure/compute things like column widths, row/array lengths, hash equality between duplicated files, and test boundary conditions. Explicitly confirm the scoped set of changed files matches expectations, and flag any invariant (e.g., 'never wraps', behavior parity) that isn't verifiably guaranteed across all consumers/code paths.
- Ensure banner-related file changes stay synchronized across duplicate implementations: When modifying the CLI banner (BANNER_ROWS, BANNER_GRADIENT, or related quadrant-block art/colors), the user expects perfect consistency between all files that duplicate this data, especially src/lib/banner.ts and src/lib/init/ui/ink-ui.ts. Always verify row counts, gradient stops, and byte-for-byte content match (e.g., via sha256 checksums) across duplicated definitions. When requesting reviews, treat this synchronization as a key correctness criterion. Also expect the user to favor incremental, tiered solutions (e.g., full/wordmark/text fallback) for responsive design problems, deferring more complex approaches (like sixel graphics) for later. When given recommended options in prompts, the user typically accepts the assistant's suggested/recommended choice rather than customizing further.
- Follow the established git workflow (branch, PR, review): Behavioral pattern detected across 6 sessions (action: enforced-workflow). The user consistently demonstrates this behavior.
- Iterate on brand/design assets with review checkpoints and strict read-only verification: User is actively managing a documentation site rebrand (logos, OG images, accent colors, wordmarks) across multiple sessions, iterating incrementally: choosing among assistant-recommended options for colors/design (preferring the 'recommended' choice), deferring some items to later commits, then circling back to complete them. User also engages in a separate strict adversarial review pass of the same PR, mandating read-only discipline (sha256 checksums before/after, byte-identical restoration, clean git status verification) and expects the assistant to catch discrepancies between PR descriptions and actual diffs. When resuming design work, expect the user to ask to 'proceed with' previously deferred/planned items rather than starting fresh. When reviewing, never modify files—always checksum before touching, verify after, and flag stale/inaccurate PR descriptions relative to the real commit diff.
- Never treat incomplete operations as successful — always surface silent failures: When reviewing code, the user explicitly asserts that incomplete operations must never report success. Examples from `debug-files upload`: `not_found` after deadline is a failure (exit 1), not success (exit 0). Symlink cycles must not hang silently. Missing requested IDs must be surfaced. Any failure path that silently exits 0 or produces no diagnostic is a blocker.
- Never uses form-data at runtime — only dev transitive dependency via @types/node-fetch: User stated never uses form-data at runtime — only dev transitive dependency via @types/node-fetch.
- Perform rigorous code-only reviews cross-checked against legacy Rust CLI: When reviewing ported TypeScript Sentry CLI features (e.g., 'build upload', VCS metadata), the user requests a critical, objective code review without writing any code. Always: (1) fetch and read the corresponding legacy Rust implementation fully to cross-check behavior/parity, (2) read all relevant TS source and test files in full, (3) run `pnpm exec tsc --noEmit` and `pnpm exec vitest run` on affected test paths and report exit codes/results, (4) produce findings categorized strictly by severity (Critical/High/Medium/Low/Nit), each with exact file:line references and a concrete, specific fix suggestion, (5) be skeptical and find real functional bugs or behavioral divergences from Rust, not style nits, (6) explicitly flag any deviations from documented CLI behavior or use cases as blockers if they break primary workflows. Do not modify code during these review sessions—only report findings and let the user decide on fixes.
- Prefers freshly-created location over existing: `installAgentSkills()` in `src/lib/agent-skills.ts` returns `results.find(location => location.created) ?? results[0] ?? null` — prefers freshly-created location over existing. Installs to `
/.agents/skills/sentry-cli/` and `/.claude/skills/sentry-cli/` only. Detection: `detectClaudeCode()` checks `existsSync(join(homeDir, '.claude'))`; installer never creates top-level agent roots — presence of root dir is the detection signal. OpenCode is detected via `OPENCODE_CLIENT` env var in `detect-agent.ts` for telemetry only — NOT for skill installation. No OpenCode skill install path exists anywhere in the repo.
- Refine ASCII/block-art CLI banner rendering with iterative visual verification: When working on the Sentry CLI's ASCII/Unicode block-art banner (src/lib/banner.ts), the user is meticulous about pixel-accurate, legible wordmark reproduction. They provide reference images (e.g., sentry-ref.webp), scrutinize letterform legibility (e.g., flagged the 'E' being unreadable), and expect the assistant to: (1) verify image dimensions/scale, (2) analyze font/duty-cycle characteristics, (3) test multiple sampling/rendering strategies (threshold-downsample, monospace grid mapping, block-glyph density), (4) compare aspect ratios and column/row counts against source, and (5) generate multiple candidate options with tradeoffs (legibility vs. scanline texture) for user selection. When reviewing related PRs, the user also enforces strict read-only discipline (sha256sum verification, no edits, clean git status). Always iterate empirically on rendering parameters and present concrete side-by-side comparisons rather than a single guess.
- Request read-only critical code reviews with severity-tagged findings: When user asks for a code review of a PR/feature (often ports of legacy Rust sentry-cli functionality into the TypeScript CLI), they explicitly forbid writing or editing code — only produce findings. Provide output organized by severity levels (Critical/High/Medium/Low/Nit), with each finding including a concrete file:line reference and a specific suggested fix. User expects a rigorous, skeptical, objective review that surfaces real bugs and correctness/security issues rather than style nits. User typically specifies exact files/functions to read in full and provides context like repo path, branch, PR number, and that the code is a port from the legacy Rust implementation for cross-checking behavioral parity. Assistant should read all specified files fully, compare against the Rust reference when applicable, and compile a structured severity-ranked report without modifying any code.
- Request rigorous port-fidelity code reviews against legacy Rust implementation: When reviewing TypeScript ports of legacy Rust sentry-cli functionality (e.g., snapshots diff, build VCS metadata), the user expects a critical, objective, findings-only code review (no code changes) that: (1) reads all relevant new files fully, (2) cross-checks logic and semantics line-by-line against the original Rust source and any third-party libraries/binaries it wrapped (e.g., odiff, pixelmatch), (3) verifies wire-format/field names, default option values, and edge-case behavior (e.g., threshold scaling, antialiasing inversion, decoder output formats) match exactly, (4) runs tsc --noEmit and relevant vitest suites as verification, and (5) organizes findings by severity (Critical/High/Medium/Low/Nit) with file:line references and concrete fixes. Assistant should proactively investigate subtle divergences from Rust behavior (e.g., missing event-payload parsing, gitignore handling differences) rather than assuming the port is equivalent.
- Require adversarial read-only subagent review for merged dependency/patch-bump PRs: When a PR bumps patched dependencies or regenerates patches (e.g., @sentry/core, @sentry/node-core, @stricli/core version bumps with custom tree-shaking patches), after it's merged the user wants a separate, critical, adversarial, read-only review performed in an isolated git worktree checked out at the merged commit with a fresh install. The review must: (1) never modify source, patch, or lockfile files—builds/tests only writing to dist/; (2) verify checksums/git status to prove read-only discipline; (3) check for dangling references in both CJS and ESM builds after export stripping; (4) verify completeness of stripped exports vs new version's additions; (5) confirm no unintended lockfile/dependency changes; (6) validate PR description's numeric claims; (7) run check:patches/tsc and assess safety-net gaps. Use a subagent (or dedicated review pass) with a detailed, prioritized checklist and deliver a clear verdict (e.g., PASS / FOLLOW-UP NEEDED / BLOCKER) at the end.
- Respect explicitly rejected approaches: Behavioral pattern detected across 7 sessions (action: rejected-approach). The user consistently demonstrates this behavior.
- Review code before committing: Behavioral pattern detected across 7 sessions (action: requested-review). The user consistently demonstrates this behavior.
- Rigorously verify pnpm patch files after dependency version bumps: When updating pinned dependencies (e.g., @sentry/core, @sentry/node-core, @stricli/core) that have corresponding pnpm patchedDependencies, the user requires thorough verification of the patch files: (1) confirm all originally stripped exports/imports are still stripped in the new version, plus any new-in-version exports that reference stripped modules are also stripped, (2) verify no dangling references remain to stripped code across all build targets (cjs/esm, index/light variants), (3) check patch hygiene — no build artifacts, no unintended sourceMappingURL edits, no whitespace-only churn, (4) enumerate all file targets touched by each patch with line numbers, and (5) verify package.json patchedDependencies key format includes the version pin. When assisting with dependency/patch updates, proactively run these completeness and hygiene checks and report exact counts/diffs rather than assuming the patch still applies correctly.
- Verify patch strip-sets with empirical cross-checks before trusting them: When maintaining patches that strip exports/modules from vendored packages (e.g., @sentry/core, @sentry/node-core) across version upgrades, the user expects rigorous empirical verification rather than assumption-based patching. This includes: reconstructing pristine pre-patch source to diff against new upstream versions; checking both CJS and ESM builds for strip parity (same names removed in both formats); verifying no dangling references (exports pointing to stripped modules that themselves weren't stripped); confirming CJS re-exports don't resolve to undefined and ESM re-exports don't throw SyntaxErrors; identifying newly-added upstream exports and deciding whether to keep or strip them; and running actual runtime checks (node --check, dynamic import) as proof rather than static review alone. When updating a patch for a new package version, always cross-verify strip-set completeness and consistency across module formats before finalizing.
- Verify pnpm lockfile dependency trees and versions before finalizing dependency changes: When updating or auditing dependencies (e.g., pnpm-lock.yaml), the user expects thorough verification of the resulting lockfile: checking transitive dependency entries (like @spotlightjs/spotlight's nested @sentry/core, @sentry/node, hono), confirming exact resolved versions, cross-referencing multiple lockfile entries for the same package (patched vs unpatched, different peer contexts), and grepping for security-relevant packages to confirm no vulnerable versions remain. When a grep or script doesn't produce expected confirmation output, re-check by inspecting the raw lockfile snapshot directly rather than trusting a single query. Always confirm both package.json version ranges and pnpm-lock.yaml resolved versions match expectations after any install/update, especially around security patches or CVE fixes.
- Verify TypeScript CLI ports against both legacy Rust source and live server implementation: When reviewing or implementing a TypeScript port of a legacy Rust CLI feature (e.g., sentry-cli snapshots download), the user expects the assistant to cross-check three sources: (1) the legacy Rust implementation being ported from, (2) the actual live Sentry server-side API endpoint source code (permissions, rate limits, error responses, side effects), and (3) the new TypeScript client code under review. The user wants a rigorous, objective comparison — not just style review — that surfaces real behavioral discrepancies (e.g., streaming-to-disk vs buffering-in-memory, missing rate-limit handling, mismatched error codes, auth token leakage risks). When asked for a review, do not write code; produce findings organized by severity with file:line references and concrete fixes, grounded in actual source inspection rather than assumptions.
- Verify TypeScript port matches legacy Rust sentry-cli behavior exactly: When porting sentry-cli functionality from Rust to TypeScript, always cross-reference the legacy Rust implementation (e.g., collect_git_metadata, find_head_sha, find_base_sha, VcsInfo struct, CI/GitHub Actions env var handling, clap flag semantics like conflicts_with) against the new TS code and tests. Actively look for behavioral discrepancies — such as flag conflict handling, error vs. silent-override behavior, and missing env var reads (e.g., GITHUB_EVENT_PATH) — and flag them as bugs/blockers rather than assuming the port is correct. Use probe tests to empirically confirm flag-parsing edge cases (single flag, combined flags, malformed flags) and compare results against Rust's documented behavior. Prioritize fidelity to legacy behavior unless an intentional deviation is explicitly decided upon.
- Verify visual/terminal output changes across all render surfaces with rigorous validation: When making changes to CLI banner/visual rendering (colors, ASCII/block art, responsive layout), the user consistently: (1) requires the assistant to check ALL rendering call sites (help.ts, ink-app.tsx, ink-ui.ts, wizard-runner.ts) since banner code is duplicated/must stay in sync across multiple files; (2) cares deeply about pixel/character-level legibility and exact visual fidelity to source art (e.g., verifying individual letterforms aren't distorted); (3) expects careful tradeoff analysis (aspect ratio, duty cycle, column width) with multiple candidate options presented before deciding; (4) sometimes requests strict read-only adversarial review workflows requiring sha256 verification of files before/after and clean git status confirmation; (5) expects existing tests to be updated in sync with visual changes. When editing shared visual assets, always search for all consumers, verify byte-identical duplication where files must mirror each other, and present tiered/graduated solutions for terminal-width responsiveness.