Skip to content
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions apps/cli-docs/src/content/docs/getting-started.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -133,8 +133,9 @@ sentry auth
You'll be given a URL and a code to enter. Once you authorize the application
in your browser, the CLI stores the OAuth credentials. When the server provides
a refresh token, the CLI refreshes the access token automatically. Persist the
Sentry CLI configuration directory (`~/.sentry/` by default, overridable with
`SENTRY_CONFIG_DIR`) across runs to keep automatic refresh working.
Sentry CLI configuration directory (`$XDG_CONFIG_HOME/sentry/`, defaulting to
`~/.config/sentry/`, overridable with `SENTRY_CONFIG_DIR`) across runs to keep
automatic refresh working.

### API Token

Expand Down Expand Up @@ -177,7 +178,7 @@ See the [Self-Hosted](../self-hosted/) guide for full setup details.

## Configuration

Credentials are stored in a SQLite database at `~/.sentry/` with restricted file permissions (mode 600) for security. See [Configuration](../configuration/) for environment variables and customization options.
Credentials are stored in a SQLite database under `$XDG_CONFIG_HOME/sentry/` (defaulting to `~/.config/sentry/`) with restricted file permissions (mode 600) for security. See [Configuration](../configuration/) for environment variables and customization options.

## Next Steps

Expand Down
15 changes: 14 additions & 1 deletion apps/cli-docs/src/fragments/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,11 +103,24 @@ The `sentry api` command also uses `--verbose` to show full HTTP request/respons

## Credential Storage

We store credentials and caches in a SQLite database (`cli.db`) inside the config directory (`~/.sentry/` by default, overridable via `SENTRY_CONFIG_DIR`). The database file and its WAL side-files are created with restricted permissions (mode 600) so that only the current user can read them. The database also caches:
We store credentials and caches in a SQLite database (`cli.db`) inside the config directory. The location follows the [XDG Base Directory specification](https://specifications.freedesktop.org/basedir/latest/): by default the CLI uses `$XDG_CONFIG_HOME/sentry` (i.e. `~/.config/sentry/` when `XDG_CONFIG_HOME` is unset), and you can override it with `SENTRY_CONFIG_DIR`. For backward compatibility, if a legacy `~/.sentry/` directory already exists it continues to be used. The database file and its WAL side-files are created with restricted permissions (mode 600) so that only the current user can read them. The database also caches:

- Organization and project defaults
- DSN resolution results
- Region URL mappings
- Project aliases (for monorepo support)

See [Credential Storage](./commands/auth/#credential-storage) in the auth command docs for more details.

## Binary Install Location

When installed via the install script, the CLI binary is placed in an XDG-aligned directory. `sentry cli setup` resolves the location in this order:

1. `SENTRY_INSTALL_DIR` — explicit override
2. `$XDG_BIN_HOME` — used when set to an absolute path, per the XDG spec
3. `~/.local/bin` or `~/bin` — when either already exists and is on your `PATH`
4. `~/.local/bin` — default fallback

Older installs placed the binary in `~/.sentry/bin`. Running `sentry cli setup` moves an existing `~/.sentry/bin` binary into the resolved install directory (updating your `PATH` and recorded install metadata to match) and migrates any legacy `~/.sentry` config data (`cli.db`, `config.json`) into the XDG config directory. Both migrations are skipped when a binary or config already exists at the target.

`sentry upgrade` keeps the binary where it currently lives — it pins `SENTRY_INSTALL_DIR` to the existing install directory so an in-place update never relocates a binary that is already on your `PATH`. Legacy config data is still migrated on upgrade; to move the binary itself to the XDG location, run `sentry cli setup` (optionally with `SENTRY_INSTALL_DIR` or `XDG_BIN_HOME` set).
2 changes: 1 addition & 1 deletion packages/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ Run `sentry --help` to see all available commands, or browse the [command refere

## Configuration

Credentials are stored in `~/.sentry/` with restricted permissions (mode 600).
Credentials are stored in `$XDG_CONFIG_HOME/sentry/` (defaulting to `~/.config/sentry/`) with restricted permissions (mode 600). A pre-existing legacy `~/.sentry/` directory is still honored, and the location can be overridden with `SENTRY_CONFIG_DIR`.

## Library Usage

Expand Down
2 changes: 1 addition & 1 deletion packages/cli/install
Original file line number Diff line number Diff line change
Expand Up @@ -337,7 +337,7 @@ trap - EXIT
# interactively — when piped (curl | bash), stdin is the pipe.
if [[ "${SENTRY_INIT:-}" == "1" ]]; then
sentry_bin=""
for dir in "${SENTRY_INSTALL_DIR:-}" "$HOME/.local/bin" "$HOME/bin" "$HOME/.sentry/bin"; do
for dir in "${SENTRY_INSTALL_DIR:-}" "${XDG_BIN_HOME:-}" "$HOME/.local/bin" "$HOME/bin" "$HOME/.sentry/bin"; do
[[ -z "$dir" ]] && continue
if [[ -x "${dir}/sentry" ]]; then
sentry_bin="${dir}/sentry"
Expand Down
109 changes: 107 additions & 2 deletions packages/cli/src/commands/cli/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,13 @@
* and the upgrade command for curl-based installs).
*/

import { existsSync, unlinkSync } from "node:fs";
import {
copyFileSync,
existsSync,
mkdirSync,
renameSync,
unlinkSync,
} from "node:fs";
import { dirname, join } from "node:path";
import { captureException } from "@sentry/node-core/light";
import type { SentryContext } from "../../context.js";
Expand All @@ -28,6 +34,7 @@ import {
getAgentSkillsPreference,
setAgentSkillsPreference,
} from "../../lib/db/defaults.js";
import { closeDatabase, resolveXdgConfigDir } from "../../lib/db/index.js";
import { setInstallInfo } from "../../lib/db/install-info.js";
import {
parseReleaseChannel,
Expand Down Expand Up @@ -80,6 +87,84 @@ function formatSetupResult(result: SetupResult): string {
return result.messages.join("\n");
}

/**
* Migrate `cli.db` (+ WAL sidecars) and the old `config.json` out of the legacy
* `~/.sentry` directory into the XDG config directory.
*
* The database is opened at CLI startup (cleanup-old-binary reads install
* info), so it must be closed before the files are moved — an open SQLite file
* cannot be renamed on Windows. Closing also invalidates the cached handle, so
* the next `getDatabase()` reopens at the new path.
*/
function migrateLegacyConfig(
homeDir: string,
env: NodeJS.ProcessEnv,
emit: Logger
): void {
const legacyDir = join(homeDir, ".sentry");
// Target the XDG location directly — resolveConfigDir keeps returning the
// legacy dir while it still holds cli.db, which would make migration a no-op.
const targetConfigDir = resolveXdgConfigDir(env, homeDir);
if (targetConfigDir === legacyDir) {
return;
}

const configFiles = ["cli.db", "cli.db-wal", "cli.db-shm", "config.json"];
const hasLegacyConfig = configFiles.some((name) =>
existsSync(join(legacyDir, name))
);
if (!hasLegacyConfig || existsSync(join(targetConfigDir, "cli.db"))) {
return;
}

closeDatabase();
mkdirSync(targetConfigDir, { recursive: true, mode: 0o700 });
for (const name of configFiles) {
const from = join(legacyDir, name);
if (existsSync(from)) {
renameSync(from, join(targetConfigDir, name));
}
Comment thread
cursor[bot] marked this conversation as resolved.
}
emit(`Config: Migrated ${legacyDir} → ${targetConfigDir}`);
}

/**
* Migrate the binary out of the legacy `~/.sentry/bin` into the XDG-aware
* install dir. Returns the new binary path when a move happened, so the caller
* can point PATH setup and recorded install info at the new location instead of
* the now-deleted legacy path.
*/
function migrateLegacyBinary(
homeDir: string,
env: NodeJS.ProcessEnv,
emit: Logger
): string | undefined {
const legacyDir = join(homeDir, ".sentry");
Comment thread
jared-outpost[bot] marked this conversation as resolved.
Outdated
const filename = getBinaryFilename();
const legacyBin = join(legacyDir, "bin", filename);
const targetDir = determineInstallDir(homeDir, env);
const targetBin = join(targetDir, filename);
if (
!existsSync(legacyBin) ||
existsSync(targetBin) ||
targetDir === join(legacyDir, "bin")
) {
Comment thread
cursor[bot] marked this conversation as resolved.
Outdated
return;
Comment thread
jared-outpost[bot] marked this conversation as resolved.
Outdated
}

mkdirSync(targetDir, { recursive: true, mode: 0o755 });
copyFileSync(legacyBin, targetBin);
try {
unlinkSync(legacyBin);
} catch {
// Leave the old binary in place if it can't be removed — the new copy
Comment thread
jared-outpost[bot] marked this conversation as resolved.
// is authoritative and setInstallInfo points upgrades at it.
}
setInstallInfo({ method: "curl", path: targetBin, version: CLI_VERSION });
emit(`Binary: Migrated ${legacyBin} → ${targetBin}`);
return targetBin;
}

/**
* Handle binary installation from a temp location.
*
Expand Down Expand Up @@ -556,7 +641,27 @@ export const setupCommand = buildCommand({
let binaryDir = dirname(binaryPath);
let freshInstall = false;

// 0. Install binary from temp location (when --install is set)
// 0. Migrate any legacy ~/.sentry config/binary into XDG locations first,
// so the steps below operate on the new paths. Config and binary migrations
// are independent — a failure in one must not skip the other.
try {
migrateLegacyConfig(homeDir, process.env, emit);
} catch (error) {
warn("Legacy config migration", error);
Comment thread
jared-outpost[bot] marked this conversation as resolved.
Outdated
}
try {
const migratedBinary = migrateLegacyBinary(homeDir, process.env, emit);
// Adopt the new location so PATH setup and recorded install info point at
// the migrated binary rather than the deleted legacy path.
if (migratedBinary) {
binaryPath = migratedBinary;
binaryDir = dirname(migratedBinary);
}
} catch (error) {
warn("Legacy binary migration", error);
Comment thread
jared-outpost[bot] marked this conversation as resolved.
Outdated
}

// 1. Install binary from temp location (when --install is set)
Comment thread
cursor[bot] marked this conversation as resolved.
if (flags.install) {
const result = await handleInstall(
process.execPath,
Expand Down
23 changes: 15 additions & 8 deletions packages/cli/src/lib/binary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {
writeFileSync,
} from "node:fs";
import { chmod, copyFile, mkdir, realpath, unlink } from "node:fs/promises";
import { delimiter, dirname, join, resolve } from "node:path";
import { delimiter, dirname, isAbsolute, join, resolve } from "node:path";
import { compare as semverCompare } from "semver";
import { getUserAgent } from "./constants.js";
import {
Expand Down Expand Up @@ -226,10 +226,11 @@ export function getBinaryPaths(installPath: string): {
* Determine the install directory for a curl-installed binary.
*
* Priority:
* 1. $SENTRY_INSTALL_DIR environment variable (if set and writable)
* 2. ~/.local/bin (if exists AND in $PATH)
* 3. ~/bin (if exists AND in $PATH)
* 4. ~/.sentry/bin (fallback; setup will handle PATH modification)
* 1. $SENTRY_INSTALL_DIR environment variable
* 2. $XDG_BIN_HOME (if set to an absolute path, per the XDG spec)
* 3. ~/.local/bin (if exists AND in $PATH)
* 4. ~/bin (if exists AND in $PATH)
* 5. ~/.local/bin (XDG-aligned fallback; setup handles PATH modification)
*
* @param homeDir - User's home directory
* @param env - Process environment variables
Expand All @@ -246,7 +247,13 @@ export function determineInstallDir(
return env.SENTRY_INSTALL_DIR;
}

// 2-3. Check well-known directories that are already in PATH
// 2. XDG_BIN_HOME override — honored only when absolute, per the XDG spec
const xdgBinHome = env.XDG_BIN_HOME;
if (xdgBinHome && isAbsolute(xdgBinHome)) {
return xdgBinHome;
}

// 3-4. Check well-known directories that are already in PATH
const candidates = [join(homeDir, ".local", "bin"), join(homeDir, "bin")];

for (const dir of candidates) {
Expand All @@ -255,8 +262,8 @@ export function determineInstallDir(
}
}

// 4. Fallback — setup will handle adding this to PATH
return join(homeDir, ".sentry", "bin");
// 5. XDG-aligned fallback — setup will handle adding this to PATH
return join(homeDir, ".local", "bin");
}

/**
Expand Down
72 changes: 66 additions & 6 deletions packages/cli/src/lib/db/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@
* bundled WASM driver (`node-sqlite3-wasm`, Node < 22.15) behind one API.
*/

import { chmodSync, mkdirSync } from "node:fs";
import { chmodSync, existsSync, mkdirSync } from "node:fs";
import { createRequire } from "node:module";
import { homedir } from "node:os";
import { join } from "node:path";
import { isAbsolute, join } from "node:path";
import { getEnv } from "../env.js";
import { logger } from "../logger.js";

Expand All @@ -21,7 +21,11 @@ import { Database } from "./sqlite.js";

export const CONFIG_DIR_ENV_VAR = "SENTRY_CONFIG_DIR";

const DEFAULT_CONFIG_DIR_NAME = ".sentry";
/** Legacy config directory name under the user's home directory (`~/.sentry`). */
const LEGACY_CONFIG_DIR_NAME = ".sentry";

/** Sub-directory used under the XDG config base directory. */
const XDG_CONFIG_SUBDIR = "sentry";

const DB_FILENAME = "cli.db";

Expand Down Expand Up @@ -69,10 +73,66 @@ function registerExitHandler(): void {
});
}

/**
* Resolve the config directory from an environment and home directory.
*
* Precedence:
* 1. `SENTRY_CONFIG_DIR` — explicit override, always wins.
* 2. Legacy `~/.sentry` — used when it already exists, so existing installs
* keep working without migration.
* 3. XDG base directory — `$XDG_CONFIG_HOME/sentry`, falling back to
* `~/.config/sentry`. Per the XDG spec, a non-absolute `XDG_CONFIG_HOME`
* is ignored.
*
* Pure and side-effect free so it can be unit-tested directly.
*/
export function resolveConfigDir(env: NodeJS.ProcessEnv, home: string): string {
const override = env[CONFIG_DIR_ENV_VAR];
if (override) {
return override;
}

const legacyDir = join(home, LEGACY_CONFIG_DIR_NAME);
// Only treat the legacy directory as a prior config install when it
// contains the actual database or the old JSON config. A bare
// `~/.sentry/bin` created by the curl installer should not block XDG.
if (
existsSync(legacyDir) &&
(existsSync(join(legacyDir, DB_FILENAME)) ||
existsSync(join(legacyDir, "config.json")))
) {
return legacyDir;
Comment thread
jared-outpost[bot] marked this conversation as resolved.
}

return resolveXdgConfigDir(env, home);
}

/**
* Resolve the XDG-compliant config directory, ignoring any legacy `~/.sentry`
* install. This is the migration *target*: `resolveConfigDir` keeps returning
* the legacy dir while it holds `cli.db`, so migration must compute the new
* location directly. Honors `SENTRY_CONFIG_DIR` and an absolute
* `XDG_CONFIG_HOME`, otherwise defaults to `~/.config/sentry`.
*/
export function resolveXdgConfigDir(
env: NodeJS.ProcessEnv,
home: string
): string {
const override = env[CONFIG_DIR_ENV_VAR];
if (override) {
return override;
}

const xdgConfigHome = env.XDG_CONFIG_HOME;
const configHome =
xdgConfigHome && isAbsolute(xdgConfigHome)
? xdgConfigHome
: join(home, ".config");
return join(configHome, XDG_CONFIG_SUBDIR);
}

export function getConfigDir(): string {
return (
getEnv()[CONFIG_DIR_ENV_VAR] || join(homedir(), DEFAULT_CONFIG_DIR_NAME)
);
return resolveConfigDir(getEnv(), homedir());
}

export function getDbPath(): string {
Expand Down
Loading
Loading