- 403/401 enrichment pipeline in infrastructure.ts — centralized, no interactive auto-fix: `src/lib/api/infrastructure.ts` centralizes HTTP error enrichment. `enrichDetail()` dispatches: 403→`enrich403Detail`, 401→`enrich401Detail`, others pass raw. `enrich403Detail()` three branches: (1) `rawDetail.includes('disabled this feature')` → org-policy message; (2) `isEnvTokenActive()` → `extractRequiredScopes(rawDetail)`; scopes found → definite missing-scope message; no scopes → hedged message; (3) OAuth → re-auth suggestion. `throwApiError()` and `throwRawApiError()` both set `ApiError.enriched403=true`. No interactive auto-fix. `buildPermissionError()` in `project/delete.ts` NEVER suggests `sentry auth login` — re-auth via OAuth won't change permissions. OAuth `auth login` always grants required scopes, so scope hints only apply to env-var tokens. 401 errors: fix is always re-authenticate — scope hints do NOT apply.
- 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 build pipeline: esbuild → fossilize → Node SEA (replacing Bun.build compile): Binary build pipeline: `src/bin.ts → [esbuild CJS, node24] → dist-build/bin.js → [fossilize --no-bundle --hole-punch] → Node SEA binary → gzip`. CRITICAL ORDER: hole-punch BEFORE signing — hole-punching after signing invalidates macOS code signature (AMFI SIGKILL). fossilize 0.8.0+ runs hole-punch via `--hole-punch` between chmod and sign+notarize. Strip debug symbols handled inside fossilize (v0.7.0+). macOS: `strip -x` on unsigned copy; cross-strip from Linux silently fails. UPX RULED OUT — destroys ELF notes. ALL_TARGETS: darwin-arm64/x64, linux-arm64/x64, win32-x64 + musl variants. Post-process: rename `sentry-win-x64.exe`→`sentry-windows-x64.exe`. `FOSSILIZE_SIGN=y` on push to main/release. `useSnapshot:true` BROKEN; `useCodeCache:true` ~15% startup improvement. fossilize 0.8.1 fixes cross-compile strip crash.
- 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`.
- E2E test infrastructure: fixture.ts, helpers.ts, mocks/, and test:e2e script: E2E test infrastructure: `test/fixture.ts`: `getCliCommand()` returns `[SENTRY_CLI_BINARY]` or `[process.execPath, 'run', 'src/bin.ts']`. `createE2EContext(configDir, serverUrl)` sets env `SENTRY_AUTH_TOKEN: ''`, `SENTRY_TOKEN: ''`, `SENTRY_CLI_NO_TELEMETRY: '1'`, `SENTRY_URL: serverUrl`. `setAuthToken(token)` calls `dbSetAuthToken(token, undefined, undefined, { host: serverUrl })` then `closeDatabase()`, scoped to mock server URL for host-scoping fetch-layer guard. `test/mocks/server.ts`: `createMockServer(routes, options?)` uses Node `http.createServer`. `test/mocks/multiregion.ts`: `createMultiRegionMockServer()` — US+EU regions + control silo; `selfHostedMode`, `singleRegionMode`. `telemetry-exit.test.ts` verifies `@sentry/core` patch adds `.unref()` to flush timers.
- Host-scoped token model: auth.host column + three-layer enforcement: Host-scoped token model (schema v16): every token bound to issuing host via `auth.host` column, lazy-migrated from boot-env. Trust established ONLY via `sentry auth login --url` or shell-exported `SENTRY_HOST`/`SENTRY_URL` at boot — `.sentryclirc` URL never a trust source. Three enforcement layers: (1) `applySentryUrlContext` throws on URL-arg mismatch; (2) `applySentryCliRcEnvShim` throws on rc-url mismatch (auth login/logout bypass via `skipUrlTrustCheck`); (3) fetch-layer `isRequestOriginTrusted`. Region trust: in-process Set in `db/regions.ts`, auto-synced by `setOrgRegion(s)`. `clearTrustedHostState` must NOT clear login anchor (breaks IAP re-auth). `HostScopeError` has overloads `(message)` and `(source, destinationUrl, tokenHost)`. Test helpers: `resetHostScopingState()` bundles `resetEnvTokenHostForTesting` + `resetLoginTrustAnchorForTesting` + `resetTrustedRegionUrlsForTesting`. E2E: pass `--url ${ctx.serverUrl}` to `auth login --token`; `SENTRY_URL` alone doesn't anchor. Multi-region tests need `registerTrustedRegionUrls`.
- InkUI vs OpenTUI decision — pure JS wins over native binary cost: InkUI vs OpenTUI decision — pure JS wins: Chose Ink over OpenTUI (OpenTUI added ~10.7 MB + FFI bindings). UI evolution: ClackUI → OpenTuiUI → InkUI (current). `exitOnCtrlC: false` routes Ctrl+C through prompt cancellation; `patchConsole: false` keeps `console.*` on real stdout. InkUI teardown order (all try/catch, `torndown` guard prevents double-unmount): (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()`. `cancelRequested` guard: second Ctrl+C → `process.exit(130)`. `createInkUI()` resolves `inkAppPath` per runtime: (1) SEA — `sea.getAsset`, write to `mkdtempSync`, import via `pathToFileURL`, then `rmSync`; (2) npm bundle — `new URL(inkAppPath, import.meta.url).href`; (3) dev — absolute path.
- SDK invoke path bypasses Stricli parsing — no defaults, no parsePeriod: `src/lib/sdk-invoke.ts` `buildInvoker()` calls command `func()` directly with pre-built flags, skipping Stricli's `parseInputsForFlag`. Stricli's default application (including `parsePeriod('90d')` for `kind:'parsed'` flags) never runs. Systemic fix: refactor `resolveCommand()` to cache BOTH the loader AND `target.parameters.flags`. In `buildInvoker`, before each `func.call`, iterate flags: for each with `kind:'parsed'` and a `default`, if SDK caller passed `undefined`, call `flag.parse(flag.default)`. `cleanRawFlags()` strips only injected globals (`log-level`, `verbose`, `org`, `project`) — `ALWAYS_STRIP = new Set([LOG_LEVEL_KEY])` strips unconditionally; command never sees `log-level`. Command-defined flags like `period`, `limit`, `query`, `json`, `fields` pass through intact. `issue list` defines its own `period` flag inline (`default: '90d'`), not via shared `LIST_PERIOD_FLAG` (`default: '7d'`).
- Sentry API: events require org+project, issues have legacy global endpoint: Sentry API quirks: (1) Events need org+project (`/projects/{org}/{project}/events/{id}/`); issues use legacy global `/api/0/issues/{id}/`; traces need org only. (2) `/users/me/` returns 403 for OAuth — use `/auth/` via `getControlSiloUrl()`. (3) Chunk upload returns camelCase (`chunkSize`) — exception to snake_case. (4) 204/205 responses throw `ApiError` not `TypeError`. (5) Magic `@` selectors: `@latest`, `@most_frequent` in `parseIssueArg`; `SELECTOR_MAP` case-insensitive. (6) `issue resolve --in`: omitted→immediate, `<version>`→inRelease, `@next`→inNextRelease, `@commit`→auto-detect git HEAD. (7) 403 is NOT retryable (only 408/429/5xx are). (8) `ApiError.enriched403=true` set by `throwApiError`/`throwRawApiError`. Repo matching uses `listRepositoriesCached` (7-day SQLite cache); always use `listAllRepositories` (paginated) — never `listRepositories` (caps ~25). `classifySilenced` only silences `ApiError` with status 401-499 — wrapping in generic `CliError` loses `status`. Re-throw via `new ApiError(msg, error.status, error.detail, error.endpoint)`.
- sentry local command: Hono+Spotlight SDK server with SSE tail output: `sentry local` (default: `serve`) and `sentry local run` — both `auth: false`. Default port 8969. Uses `@spotlightjs/spotlight/sdk` (`createSpotlightBuffer`/`pushToSpotlightBuffer`) for envelope buffering; custom Hono HTTP server for ingest. Endpoints: `POST /stream`, `POST /api/:projectId/envelope[/]`, `GET /stream` (SSE), `GET /health`. CORS restricted to localhost origins only. Browser SDK `sendBeacon` workaround: overrides `text/plain` → `application/x-sentry-envelope` when `sentry_client` query param starts with `sentry.javascript.browser`. `sentry local run` injects `SENTRY_SPOTLIGHT`, `NEXT_PUBLIC_SENTRY_SPOTLIGHT`, `SENTRY_TRACES_SAMPLE_RATE=1` into child env. Attach mode: if server already running, connects as SSE consumer (manual SSE parser — no `EventSource`). Formatters in `src/lib/formatters/local.ts`: `sanitize()` strips ANSI/control/bidi chars; source inferred from `sdk.name` → `[SERVER]`/`[BROWSER]`/`[MOBILE]`.
- src/bin.ts: SQLite warning suppression, stream error handling, and safety net: `src/bin.ts`: (1) SQLite `ExperimentalWarning` suppressed by patching `process.emit` BEFORE any import — filters `event==='warning'` + `args[0].name==='ExperimentalWarning'` + `args[0].message.includes('SQLite')`. (2) `handleStreamError(err)`: `EPIPE`→`process.exit(0)`; `EIO`→`process.exit(1)`; others re-thrown. Registered on both `process.stdout` and `process.stderr`. (3) `startCli().catch(() => { process.exitCode = 1; })` is a safety net only. (4) `src/cli.ts` middleware order: `[seerTrialMiddleware, rcImportMiddleware, autoAuthMiddleware]` — innermost-first so `autoAuthMiddleware` is outermost. `rcImportMiddleware`: fires on `AuthError{reason:'not_authenticated'}` + `!skipAutoAuth` + `isatty(0)`. `autoAuthMiddleware`: catches `not_authenticated|expired` + `!skipAutoAuth` + `isatty(0)` (not `process.stdin.isTTY`). `promptImportConsent()`: only `false`→`'declined'` permanently suppresses.
- 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.
- --json schema stability: collapse=organization drops nested org fields: --json schema + response cache gotchas: (1) `?collapse=organization` shrinks `organization` to `{id,slug}` — silent --json regression. `jsonTransform` re-hydrates `organization.name` via `resolveOrgDisplayName` against `org_regions` cache. (2) `buildCacheKey()` normalizes URL with sorted query params, so `invalidateCachedResponse(baseUrl)` misses entries with query suffixes. Use `invalidateCachedResponsesMatching(prefix)` (raw `startsWith()`); `buildApiUrl()` always emits trailing slash → safe prefix. (3) When `jsonTransform` is set, `jsonExclude` and `filterFields` are NOT applied — transform must call `filterFields(result, fields)` and omit excluded keys itself. (4) `atomicWriteCacheFile` writes to `${finalPath}.${process.pid}.${randomUUID()}.tmp` then renames — atomic on POSIX, near-atomic on Windows (same volume). Orphaned `.tmp` files on crash not cleaned by `cleanupCache()` (filters `.json` only); swept by stale-tmp pass (>60s threshold). Corrupt files pushed to `entries` with `expired:true` so eviction counts them.
- 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`).
- Multi-region fan-out: distinguish all-403 from empty orgs with hasSuccessfulRegion flag: In `listOrganizationsUncached` (`src/lib/api/organizations.ts`), `Promise.allSettled` collects multi-region results. Don't use `flatResults.length === 0` to detect all-regions-failed — a region returning 200 OK with zero orgs pushes nothing into `flatResults`. Track a `hasSuccessfulRegion` boolean on any `"fulfilled"` settlement. Only re-throw 403 `ApiError` when `!hasSuccessfulRegion && lastScopeError`.
- 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.
- 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.
- Vitest worker pool requires pool:forks + UV_USE_IO_URING=0 on GitHub Actions: Vitest/CI gotchas: (1) GitHub Actions io_uring crashes Node.js workers (exit 134/SIGABRT) — fix: `pool: 'forks'` in `vitest.config.ts` AND `UV_USE_IO_URING=0` in CI. (2) Vitest 4: options must be second arg: `test(name, { timeout }, fn)`. (3) `http.createServer(async ...)` — unhandled rejections crash test server; wrap body in try/catch. (4) `node:sqlite` requires `--experimental-sqlite` on Node 22. (5) Lazy `require()` in test fixtures bypasses Vite's `.js→.ts` resolver — use top-level `import`. (6) `spawn(process.execPath, [workerScript.ts])` fails under vitest/Node — use `spawn('tsx', [workerScript.ts])`. (7) ALL test files MUST import from `'vitest'` — NEVER `'bun:test'`. `test:e2e` runs WITHOUT `--isolate --parallel`; `test:unit` runs WITH. `mock.module()` pollutes module registry — put in `test/isolated/`.
- Whole-buffer matchAll slower than split+test when aggregated over many files: Grep/scan traps in `src/lib/scan/`: (1) Literal prefilter is FILE-LEVEL gate; per-line verify breaks cross-newline patterns. (2) `hasTopLevelAlternation`+`skipGroup` must call `skipCharacterClass`. (3) Wake-latch race: use latched `pendingWake` flag. (4) `mapFilesConcurrent` filters `null` but NOT `[]` — return `null` for no-op files. (5) `collectGlob`/`collectGrep` must NOT forward `maxResults` to iterator. Worker pool: lazy singleton, size `min(8, max(2, availableParallelism()))`. Matches encoded as `Uint32Array` quads (~40% faster). `new Worker(new URL(...))` HANGS in SEA binaries — use Blob+URL.createObjectURL. `ref()`/`unref()` idempotent — only unref when `inflight` drops to 0. Disable via `SENTRY_SCAN_DISABLE_WORKERS=1`.
- Sentry SDK tree-shaking patches must be regenerated via bun patch workflow: Sentry SDK tree-shaking via pnpm patch: `patchedDependencies` in `package.json` under `pnpm` config block strips unused exports from `@sentry/core` and `@sentry/node-core`. Always import from `@sentry/node-core/light`. Bumping SDK: remove old patches, `pnpm patch @sentry/core`, edit, `pnpm patch-commit`; repeat for node-core. `check:patches` validates version alignment AND content (greps installed files for known-bad strings). `@stricli/core` patch targets `dist/index.js` (ESM) only — `dist/index.cjs` intentionally unpatched. When bumping `@stricli/core`, patch line numbers shift — always verify offsets against the new `dist/index.js`. KNOWN: Stricli `-H` removal patch is fragile — any Stricli version bump will break it; upstream fix will NOT be accepted.
- 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. Hidden global `--org`/`--project` flags: defined in `GLOBAL_FLAGS`, `mergeGlobalFlags()` injects hidden flag shapes, `applyOrgProjectFlags()` writes to `SENTRY_ORG`/`SENTRY_PROJECT` before auth guard. `applyGroupLimitAutoDefault` helper keeps `buildCommand` under Biome's cognitive complexity limit of 15.
- Testing Stricli command func() bodies via spyOn mocking: Testing Stricli command func() bodies: `const func = await cmd.loader(); func.call(mockContext, flags, ...args)` with mock `stdout`, `stderr`, `cwd`, `setContext`. `.call()` LSP false-positives pass `tsc --noEmit`. When API functions are renamed, update both spy target AND mock return shape. `normalizeSlug` replaces `_`→`-` but does NOT lowercase. Vitest: use `vi.spyOn` / mock fetch via `globalThis.fetch`. `mock.module()` pollutes module registry — put in `test/isolated/`. ALL test files MUST import from `'vitest'` — NEVER `'bun:test'`. Variadic flag pattern: `kind: 'parsed', parse: String, variadic: true, optional: true`. Aliases: `aliases: { s: 'scope' }` — place INSIDE `parameters` (sibling to `flags`), NOT at top-level of `buildCommand` options (causes TS2353). `-s` is free in `auth/login.ts`.
- Always migrate Bun-specific APIs and tooling to Node.js equivalents: Bun→Node.js migration complete. Replace Bun APIs: `Bun.spawn`→`node:child_process`, `Bun.sleep`→`node:timers/promises`, `bun:sqlite`→`node:sqlite`, `bun run`→`pnpm run`/`tsx`, `Bun.file().text()`→`readFile(path,'utf-8')`, `Bun.write()`→`writeFile()`, `Bun.which()`→Node-compatible pkg, `Bun.Glob`→`tinyglobby`/`picomatch`, `Bun.randomUUIDv7()`→`uuidv7`, `Bun.semver.order()`→`semver.compare()`, `Bun.zstdCompressSync()`→zlib/`zstd-napi`. Exception: `script/build.ts` uses fossilize (not `Bun.build`) and stays on Bun for build-binary CI job. `script/bundle.ts` uses esbuild via tsx. `packageManager`: `pnpm@10.11.0`. bun.lock deleted, vitest.config.ts added. `.npmrc`: `node-linker=hoisted`. `patchedDependencies` moved to `pnpm` config block. `NODE_VERSION='lts'`. `new Worker(new URL(...))` HANGS in SEA — use Blob+URL.createObjectURL.
- Prefers Bun-native APIs; use buildCommand from lib/command.js (never @stricli/core directly); use buildRouteMap from lib/route-map.js; silent catch blocks prohibited; every new src/lib/**/*.ts must start with module-level JSDoc; test isolation via useTestConfigDir(); prefer property-based and model-based tests over unit tests; DEFAULT_NUM_RUNS = 50; architecture tree documented; error exit code ranges: 1x=auth: Project conventions (AGENTS.md): Use `pnpm run`/`pnpm install`/`pnpm add -D`. Use `buildCommand` from `lib/command.js` (NEVER `@stricli/core` directly); `buildRouteMap` from `lib/route-map.js`. Silent catch blocks prohibited — every catch must re-throw, call `log.debug()`, or return a fallback with `log.debug()`. Every new `src/lib/**/*.ts` must start with module-level JSDoc. Test isolation via `useTestConfigDir()`. Prefer property-based/model-based tests (fast-check); `DEFAULT_NUM_RUNS=50`. Error exit codes: 1x=auth, 2x=input/config, 3x=API/network, 4x=feature/billing, 5x=operations, 6x=command-specific. Use `EXIT.*` constants — never hardcode numeric exit codes. All packages in `devDependencies`. NEVER merge if CI failing. Add new check scripts to BOTH `package.json` AND `.github/workflows/ci.yml`. `bun run lint` for local lint. `return undefined` → bare `return`. ALL test files MUST import from `'vitest'` — NEVER `'bun:test'`. Always write tests alongside implementation. Document design decisions in `.lore.md`.
- Telemetry implementation invariants: handler cleanup, uid check, non-blocking, redaction: Telemetry invariants (`src/lib/telemetry.ts`): (1) `initSentry()` ALWAYS removes `currentBeforeExitHandler` before registering new one. (2) `isOwnedByRoot()` returns `false` immediately on Windows. (3) NEVER block CLI execution — all drains are best-effort, wrapped in try/catch. (4) `SENSITIVE_ARGV_FLAGS` (`token`, `auth-token`) NEVER sent — `redactArgv()` handles both `--flag=value` and `--flag <value>` forms; raw `process.argv` must NEVER reach telemetry without `redactArgv()`. `runCompletion()` sets `SENTRY_CLI_NO_TELEMETRY=1` to skip `@sentry/node-core` lazy-require (~280ms). `reportUnknownCommand()` wrapped in try/catch. `SENSITIVE_ARGV_FLAGS` in `cli.ts` is a superset of `SENSITIVE_FLAGS` in `telemetry.ts`. tool_calls table (v31): call_id, tool, status, error_type, error_message, duration_ms, session_id, project_path.