Skip to content

feat(cli): add sentry cli import for .sentryclirc migration#987

Merged
BYK merged 2 commits into
mainfrom
feat/sentryclirc-import
May 20, 2026
Merged

feat(cli): add sentry cli import for .sentryclirc migration#987
BYK merged 2 commits into
mainfrom
feat/sentryclirc-import

Conversation

@BYK
Copy link
Copy Markdown
Member

@BYK BYK commented May 20, 2026

Summary

Adds a one-time import path for users migrating from the old Rust-based sentry-cli, addressing #975. Two complementary features share a core import engine:

Feature A: sentry cli import — Explicit Command

Scans for .sentryclirc files, shows what was found (masked token, URL, org, project with file provenance), and imports settings into SQLite with proper host scoping.

sentry cli import               # Scan and import interactively
sentry cli import --yes         # Auto-confirm (CI-safe)
sentry cli import --dry-run     # Preview without changes
sentry cli import --url <url>   # Trust a specific URL (bypasses same-file rule)
sentry cli import --skip-validation  # Skip API token validation

Feature B: Auto-Detect Middleware

When any command hits AuthError("not_authenticated") and a non-project-local .sentryclirc has a token that passes the trust gate, prompts to import before falling back to the OAuth login flow. Disabled in CI (isatty check). User can decline once (never asked again).

Security: Content-Based Trust Model

No file path is inherently trusted (on CI, even ~/.sentryclirc can be planted). Trust is determined by the same-file rule: the effective token and URL must originate from the same .sentryclirc file (co-presence means an attacker who planted both already has the token — nothing to steal via redirect).

Scenario Trusted?
Token + URL in same file Yes (co-presence)
Token only, no URL Yes (SaaS default)
Token in file A, URL in file B No — requires --url

SHA-256 hash change detection: File content hashes stored at import time. Post-import mutations (e.g., attacker appends URL) clear the record and trigger re-evaluation.

Login Hint

Updates rcTokenHint to mention sentry cli import as an alternative to --token.

Changes

  • src/lib/sentryclirc-import.ts (new) — Core import engine: discover, classify, plan (merge + trust analysis), execute (store + validate), state tracking (hash-verified completion, decline)
  • src/commands/cli/import.ts (new) — sentry cli import command with formatters
  • src/commands/cli/index.ts — Register import route
  • src/cli.ts — Add rcImportMiddleware + tryRcImport() in error middleware chain
  • src/lib/sentryclirc.ts — Export getGlobalPaths() and tryReadSentryCliRc()
  • src/commands/auth/login.ts — Update rcTokenHint to mention import command
  • test/lib/sentryclirc-import.test.ts (new) — 31 unit tests
  • test/lib/sentryclirc-import.property.test.ts (new) — 7 property-based tests

Closes #975

@BYK BYK self-assigned this May 20, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 20, 2026

PR Preview Action v1.8.1

QR code for preview link

🚀 View preview at
https://cli.sentry.dev/_preview/pr-987/

Built to branch gh-pages at 2026-05-20 22:03 UTC.
Preview will be ready when the GitHub Pages deployment is complete.

Comment thread src/lib/sentryclirc-import.ts
Comment thread src/commands/cli/import.ts
Comment thread src/commands/cli/import.ts Outdated
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 20, 2026

Codecov Results 📊

7075 passed | Total: 7075 | Pass Rate: 100% | Execution Time: 0ms

📊 Comparison with Base Branch

Metric Change
Total Tests 📈 +61
Passed Tests 📈 +61
Failed Tests
Skipped Tests

All tests are passing successfully.

❌ Patch coverage is 44.68%. Project has 14819 uncovered lines.
❌ Project coverage is 76.56%. Comparing base (base) to head (head).

Files with missing lines (4)
File Patch % Lines
src/commands/cli/import.ts 16.19% ⚠️ 233 Missing
src/lib/sentryclirc-import.ts 69.62% ⚠️ 168 Missing
src/cli.ts 0.00% ⚠️ 138 Missing
src/commands/auth/login.ts 33.33% ⚠️ 2 Missing
Coverage diff
@@            Coverage Diff             @@
##          main       #PR       +/-##
==========================================
- Coverage    77.10%    76.56%    -0.54%
==========================================
  Files          322       324        +2
  Lines        62208     63212     +1004
  Branches         0         0         —
==========================================
+ Hits         47963     48393      +430
- Misses       14245     14819      +574
- Partials         0         0         —

Generated by Codecov Action

@BYK BYK force-pushed the feat/sentryclirc-import branch from 613ee43 to 5be20aa Compare May 20, 2026 17:07
@BYK
Copy link
Copy Markdown
Member Author

BYK commented May 20, 2026

Post-Review Fixes

Addressed findings from a thorough self-review:

Critical fixes

  • C1: executeImport now guards setAuthToken on plan.newFields.includes("token") — prevents silently overwriting existing OAuth credentials with a stale .sentryclirc token
  • C2: applyUrlOverride now recalculates plan.isSaas when --url is passed — fixes stale state that prevented URL from being added to newFields
  • C3: All catch blocks now log via log.debug() per codebase rules — no more silent error swallowing
  • C4: Replaced process.stderr.write() with log.info() in command file per GritQL lint rule

Medium fixes

  • M5: HostScopeError in middleware catch now logs at warn level (security-relevant), others at debug

Minor fixes

  • N1: maskToken now fully masks tokens <= 8 chars (was leaking full token for short inputs)
  • N6: parseImportUrl now throws ValidationError instead of plain Error (proper exit code 21)
  • N3: parseImportRecord now validates Array.isArray(sources) before casting

Comment thread src/commands/cli/import.ts Outdated
@BYK BYK force-pushed the feat/sentryclirc-import branch from de6ce74 to fed87ea Compare May 20, 2026 17:13
Comment thread src/cli.ts Outdated
Comment thread src/lib/sentryclirc-import.ts
Comment thread src/cli.ts Outdated
Comment thread src/lib/sentryclirc-import.ts Outdated
Comment thread src/commands/cli/import.ts Outdated
Comment thread src/commands/cli/import.ts
@BYK BYK force-pushed the feat/sentryclirc-import branch from 4b23d81 to 6124f60 Compare May 20, 2026 17:29
Comment thread src/commands/cli/import.ts Outdated
Comment thread src/lib/sentryclirc-import.ts Outdated
Comment thread src/lib/sentryclirc-import.ts
@BYK BYK force-pushed the feat/sentryclirc-import branch from 2f767d2 to f4c287a Compare May 20, 2026 17:44
Comment thread src/cli.ts
Comment thread src/lib/sentryclirc-import.ts Outdated
Comment thread src/cli.ts
Comment thread src/commands/cli/import.ts
@BYK BYK force-pushed the feat/sentryclirc-import branch from 7c8e024 to 11bb64d Compare May 20, 2026 17:52
Comment thread src/lib/sentryclirc-import.ts Outdated
Comment thread src/lib/sentryclirc-import.ts Outdated
Comment thread src/lib/sentryclirc-import.ts Outdated
@BYK BYK force-pushed the feat/sentryclirc-import branch from f6f7f59 to ff3f635 Compare May 20, 2026 17:57
Comment thread src/commands/cli/import.ts
@BYK BYK force-pushed the feat/sentryclirc-import branch from d9f9654 to 32d44fb Compare May 20, 2026 18:04
Comment thread src/lib/sentryclirc-import.ts
Comment thread src/commands/cli/import.ts
Comment thread src/lib/sentryclirc-import.ts
Comment thread src/lib/sentryclirc-import.ts
@BYK BYK force-pushed the feat/sentryclirc-import branch from 46b6f80 to ad645a7 Compare May 20, 2026 18:11
Comment thread src/lib/sentryclirc-import.ts Outdated
Comment thread src/lib/sentryclirc-import.ts Outdated
Comment thread src/commands/cli/import.ts Outdated
Comment thread src/lib/sentryclirc-import.ts
@BYK BYK force-pushed the feat/sentryclirc-import branch from 1cb4cc6 to 967798e Compare May 20, 2026 18:19
Comment thread src/commands/cli/import.ts
Comment thread src/commands/cli/import.ts Outdated
Comment thread src/commands/cli/import.ts
Comment thread src/cli.ts
@BYK BYK force-pushed the feat/sentryclirc-import branch from 967798e to a0547d3 Compare May 20, 2026 18:52
Comment thread src/lib/sentryclirc-import.ts
@BYK BYK enabled auto-merge (squash) May 20, 2026 21:57
Add a one-time import path for users migrating from the old Rust-based
sentry-cli. Two complementary features:

1. `sentry cli import` — explicit command that scans for .sentryclirc
   files, shows what was found, and imports settings into SQLite with
   proper host scoping. Supports --yes (CI), --dry-run, --url (trust
   override), and --skip-validation.

2. Auto-detect middleware — when any command hits AuthError and a
   .sentryclirc token passes the trust gate, prompts to import before
   falling back to the OAuth login flow.

Security: content-based trust model (same-file rule). Token and URL
must originate from the same file — no path is inherently trusted.
SHA-256 file hashes stored at import time detect post-import tampering.
Auto-prompt disabled in CI (isatty check); project-local files excluded.

Also updates the login command's rcTokenHint to mention `sentry cli import`
as an alternative to `--token`.
@BYK BYK force-pushed the feat/sentryclirc-import branch from a0547d3 to 43c2e22 Compare May 20, 2026 22:02
@BYK BYK merged commit 95ff2e3 into main May 20, 2026
30 checks passed
@BYK BYK deleted the feat/sentryclirc-import branch May 20, 2026 22:09
Copy link
Copy Markdown
Contributor

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit d5a0667. Configure here.

return "*".repeat(token.length);
}
return `${token.slice(0, 4)}${"*".repeat(token.length - 8)}${token.slice(-4)}`;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Duplicate maskToken with divergent behavior across files

Medium Severity

A new maskToken is exported from sentryclirc-import.ts while an existing maskToken already exists in src/lib/formatters/human.ts. They have different behavior: the existing one returns "****" for short tokens and shows first 8 + last 4 for long tokens; the new one returns "*".repeat(length) for short tokens and shows first 4 + last 4 for long tokens. Two exported functions with the same name but different masking semantics in the same codebase creates confusion about which to use and risks inconsistent token display.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit d5a0667. Configure here.

plan.newFields.push("url");
}
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale cross-file warnings persist after --url override

Low Severity

applyUrlOverride mutates the plan's effective.url, effectiveSources, trusted, and isSaas fields but does not clear stale warnings from plan.warnings that buildSecurityWarnings already built from the original plan state. When --url is passed to resolve a cross-file trust issue, the user still sees the "Token and URL come from different files" warning — even though --url exists precisely to bypass that concern. Similarly, a stale sntrys_ claim mismatch warning about the original URL may remain alongside a new one for the override URL.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit d5a0667. Configure here.

Comment thread src/commands/cli/import.ts
Comment on lines +791 to +793
// If auth was cleared since the import (e.g., logout), re-offer import
if (!hasStoredAuth()) {
return true;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

markImportDeclined never breaks re-prompt loop when a prior import record exists

When token validation fails inside executeImport, markImportDeclined is called to suppress future auto-prompts, but the existing completed import record is never cleared. On every subsequent command, isImportNeededAsync finds the old import record first, sees no stored auth, and returns true again — the decline record branch is only reached when there is no import record. The user is stuck in an infinite auto-prompt loop until they fix the token or explicitly run sentry cli import.

Evidence
  • isImportNeededAsync (line 784): checks getImportRecord() first; if a record exists, the function returns without ever consulting the decline record.
  • When hasStoredAuth() returns false (line 792) — e.g., after a 401 cleared auth — the function returns true (import needed), regardless of any decline record.
  • executeImport (line ~654, confirmed read): calls markImportDeclined(plan.sources) after validation failure but does not call clearImportRecord(), leaving the old completed import record in the DB.
  • The inline comment on that same line reads "Declined is checked before hasStoredAuth, breaking the loop" — this is incorrect; the decline record is only consulted in the else branch at line 799, which is unreachable when the import record exists.
  • Reproducer: successful import → logout → re-prompt fires → user accepts → token is invalid (401) → markImportDeclined runs → next run: old import record still present, no auth → isImportNeededAsync returns true → loop.

Identified by Warden find-bugs · L3M-JM9

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

migration from sentry-cli .sentryclirc

1 participant