Skip to content
Open
Show file tree
Hide file tree
Changes from all 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 @@ -112,8 +112,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 @@ -156,7 +157,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
2 changes: 1 addition & 1 deletion apps/cli-docs/src/fragments/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ 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
Expand Down
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
53 changes: 47 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,47 @@ 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.
}

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
62 changes: 60 additions & 2 deletions packages/cli/test/lib/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@
* Integration tests for SQLite-based config storage.
*/

import { writeFileSync } from "node:fs";
import { access } from "node:fs/promises";
import { mkdirSync, writeFileSync } from "node:fs";
import { access, mkdtemp, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, beforeEach, describe, expect, test } from "vitest";
import {
Expand All @@ -26,6 +27,7 @@ import {
CONFIG_DIR_ENV_VAR,
closeDatabase,
getDbPath,
resolveConfigDir,
} from "../../src/lib/db/index.js";
import {
clearProjectAliases,
Expand Down Expand Up @@ -561,6 +563,62 @@ describe("getDbPath", () => {
});
});

describe("resolveConfigDir", () => {
let home: string;

beforeEach(async () => {
home = await mkdtemp(join(tmpdir(), "resolve-config-home-"));
});

afterEach(async () => {
await rm(home, { recursive: true, force: true });
});

test("prefers the SENTRY_CONFIG_DIR override over everything", () => {
const override = join(home, "custom-config");
mkdirSync(join(home, ".sentry"));
expect(
resolveConfigDir(
{
[CONFIG_DIR_ENV_VAR]: override,
XDG_CONFIG_HOME: join(home, "xdg"),
},
home
)
).toBe(override);
});

test("uses the legacy ~/.sentry directory when it already exists", () => {
const legacy = join(home, ".sentry");
mkdirSync(legacy);
writeFileSync(join(legacy, "cli.db"), ""); // simulate a prior config install
expect(resolveConfigDir({}, home)).toBe(legacy);
});

test("ignores a bare ~/.sentry/bin (installer artifact) and falls back to XDG", () => {
const legacy = join(home, ".sentry");
mkdirSync(join(legacy, "bin"), { recursive: true });
expect(resolveConfigDir({}, home)).toBe(join(home, ".config", "sentry"));
});

test("uses XDG_CONFIG_HOME/sentry when set to an absolute path", () => {
const xdg = join(home, "xdg-config");
expect(resolveConfigDir({ XDG_CONFIG_HOME: xdg }, home)).toBe(
join(xdg, "sentry")
);
});

test("falls back to ~/.config/sentry when XDG_CONFIG_HOME is unset", () => {
expect(resolveConfigDir({}, home)).toBe(join(home, ".config", "sentry"));
});

test("ignores a non-absolute XDG_CONFIG_HOME per the XDG spec", () => {
expect(resolveConfigDir({ XDG_CONFIG_HOME: "relative/path" }, home)).toBe(
join(home, ".config", "sentry")
);
});
});

// ─────────────────────────────────────────────────────────────────────────────
// JSON Migration
// ─────────────────────────────────────────────────────────────────────────────
Expand Down
Loading