Sentry CLI uses OAuth 2.0 device flow (RFC 8628) for secure authentication without requiring a browser redirect. Tokens are stored locally in SQLite with automatic refresh support.
Authentication Methods
The CLI supports three authentication methods, checked in this priority order:
SENTRY_AUTH_TOKEN environment variable (highest priority)
SENTRY_TOKEN environment variable (legacy)
- OAuth tokens stored in SQLite (lowest priority)
Environment Variable Authentication
The simplest way to authenticate is via environment variables:
export SENTRY_AUTH_TOKEN="your-token-here"
sentry issue list
Tokens from environment variables:
- Bypass all expiry and refresh logic
- Are never written to disk
- Take priority over stored OAuth tokens
- Empty or whitespace-only values are treated as unset
SENTRY_AUTH_TOKEN takes precedence over SENTRY_TOKEN to match the legacy sentry-cli behavior.
OAuth Device Flow
The recommended authentication method is OAuth device flow:
This initiates the device authorization flow:
- CLI requests a device code from Sentry’s
/oauth/device/code/ endpoint
- CLI displays a user code and verification URL
- User visits the URL and enters the code (or scans QR code)
- CLI polls
/oauth/token/ endpoint until user completes authorization
- Access token and refresh token are stored in SQLite
Device Flow Implementation
The device flow is implemented in src/lib/oauth.ts:
export async function performDeviceFlow(
callbacks: DeviceFlowCallbacks,
timeout = 600_000 // 10 minutes
): Promise<TokenResponse> {
// Step 1: Request device code
const {
device_code,
user_code,
verification_uri,
verification_uri_complete,
interval,
} = await requestDeviceCode();
// Step 2: Show user code
await callbacks.onUserCode(
user_code,
verification_uri,
verification_uri_complete
);
// Step 3: Poll for token
let pollInterval = interval;
while (Date.now() < timeoutAt) {
await sleep(pollInterval * 1000);
const result = await attemptPoll(device_code);
switch (result.status) {
case "success":
return result.token;
case "slow_down":
pollInterval += 5; // Increase interval if rate-limited
continue;
}
}
}
OAuth Scopes
The CLI requests the following scopes:
project:read - Read project data
project:write - Write project data
org:read - Read organization data
event:read - Read events
event:write - Write events
member:read - Read organization members
team:read - Read team data
Token Storage
OAuth tokens are stored in SQLite at ~/.sentry/config.db:
CREATE TABLE auth (
id INTEGER PRIMARY KEY,
token TEXT,
refresh_token TEXT,
expires_at INTEGER,
issued_at INTEGER,
updated_at INTEGER
);
The auth table uses a single-row pattern (always id = 1) to store the active credential.
Token Retrieval Priority
The getAuthToken() function checks sources in priority order:
export function getAuthToken(): string | undefined {
// 1. Check environment variables first
const envToken = getEnvToken();
if (envToken) {
return envToken.token;
}
// 2. Fall back to SQLite-stored token
const db = getDatabase();
const row = db.query("SELECT * FROM auth WHERE id = 1").get();
if (!row?.token) {
return undefined;
}
// Verify token hasn't expired
if (row.expires_at && Date.now() > row.expires_at) {
return undefined;
}
return row.token;
}
Automatic Token Refresh
OAuth tokens are automatically refreshed when:
- Less than 10% of the token’s lifetime remains (default threshold)
- A 401 Unauthorized response is received from the API
- Forced via
sentry auth refresh
Refresh Flow
The refresh flow is implemented in src/lib/db/auth.ts:
export async function refreshToken(
options: RefreshTokenOptions = {}
): Promise<RefreshTokenResult> {
// Environment tokens never expire
const envToken = getEnvToken();
if (envToken) {
return { token: envToken.token, refreshed: false };
}
const row = db.query("SELECT * FROM auth WHERE id = 1").get();
const remainingRatio = (expiresAt - now) / (expiresAt - issuedAt);
// Refresh if less than 10% lifetime remains
if (remainingRatio > REFRESH_THRESHOLD && now < expiresAt) {
return { token: row.token, refreshed: false };
}
// Use refresh token to get new access token
const tokenResponse = await refreshAccessToken(row.refresh_token);
await setAuthToken(
tokenResponse.access_token,
tokenResponse.expires_in,
tokenResponse.refresh_token
);
return {
token: tokenResponse.access_token,
refreshed: true,
expiresIn: tokenResponse.expires_in
};
}
Token refresh uses a singleton promise to prevent concurrent refresh attempts during parallel API calls.
Self-Hosted Sentry
For self-hosted Sentry instances, configure both URL and client ID:
export SENTRY_URL="https://sentry.yourcompany.com"
export SENTRY_CLIENT_ID="your-oauth-client-id"
sentry auth login
Creating an OAuth App
To use OAuth with self-hosted Sentry (requires 26.1.0+):
- Navigate to Settings → Developer Settings in your Sentry instance
- Create a new public OAuth application
- Copy the Client ID
- Set
SENTRY_CLIENT_ID environment variable
OAuth device flow requires Sentry 26.1.0 or later. For older versions, use sentry auth login --token with an API token instead.
Configuration Resolution
The CLI reads configuration lazily (not at module load) to respect environment variables set after import:
function getSentryUrl(): string {
return process.env.SENTRY_URL ?? "https://sentry.io";
}
function getClientId(): string {
return process.env.SENTRY_CLIENT_ID ?? SENTRY_CLIENT_ID_BUILD;
}
This allows URL parsing from command arguments to set SENTRY_URL before the OAuth flow begins.
Authentication State Commands
Check Status
Shows the current authentication state and token source.
Manual Refresh
Forces a token refresh even if the current token hasn’t expired.
Logout
Clears stored tokens and related cached data:
- Auth tokens
- User info cache
- Organization region cache
- Pagination cursors
Logging out only clears SQLite-stored tokens. Environment variable tokens remain active.
Token Source Tracking
The CLI tracks where each token originated via the AuthSource type:
type AuthSource =
| "env:SENTRY_AUTH_TOKEN"
| "env:SENTRY_TOKEN"
| "oauth";
type AuthConfig = {
token: string;
refreshToken?: string;
expiresAt?: number;
issuedAt?: number;
source: AuthSource;
};
This allows commands to conditionally skip refresh logic for environment tokens and provide accurate status messages.
Error Handling
Authentication errors use the AuthError class with specific reason codes:
type AuthErrorReason =
| "not_authenticated" // No token found
| "expired" // Token expired and can't refresh
| "invalid"; // Token rejected by API
throw new AuthError(
"expired",
"Session expired. Run 'sentry auth login' to re-authenticate."
);
The CLI automatically triggers the login flow when AuthError is thrown, providing a seamless authentication experience.