Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
46 changes: 40 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,40 @@ 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);
if (existsSync(legacyDir)) {
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
54 changes: 52 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,54 @@ 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", () => {
mkdirSync(join(home, ".sentry"));
expect(resolveConfigDir({}, home)).toBe(join(home, ".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