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
2 changes: 1 addition & 1 deletion apps/cli-docs/src/content/docs/agent-guidance.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ The `sentry` CLI follows conventions from well-known tools — if you're familia
## Safety Rules

<!-- GENERATED:START destructive-commands -->
- Always confirm with the user before running destructive commands: `alert issues delete`, `alert metrics delete`, `cli uninstall`, `dashboard widget delete`, `integration delete`, `issue merge`, `issue unlink`, `project delete`, `release delete`, `trial start`
- Always confirm with the user before running destructive commands: `alert issues delete`, `alert metrics delete`, `cli uninstall`, `dashboard widget delete`, `integration delete`, `issue merge`, `issue unlink`, `project delete`, `release delete`, `token delete`, `trial start`
<!-- GENERATED:END destructive-commands -->
- For mutations, verify the org/project context looks correct in the command output before proceeding with further changes
- Never store or log authentication tokens — the CLI manages credentials automatically
Expand Down
1 change: 1 addition & 0 deletions apps/cli-docs/src/content/docs/contributing.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ toolkit/
│ │ │ ├── span/ # list, view
│ │ │ ├── status/ # show
│ │ │ ├── team/ # list
│ │ │ ├── token/ # create, delete, list
│ │ │ ├── trace/ # list, logs, view
│ │ │ ├── trial/ # list, start
│ │ │ ├── api.ts # Make an authenticated API request
Expand Down
23 changes: 23 additions & 0 deletions apps/cli-docs/src/fragments/commands/token.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@


## Permissions

Sentry only lets a CLI login from `sentry auth login` manage org auth tokens.
Personal tokens, org auth tokens, and `SENTRY_AUTH_TOKEN` can't create, list,
or revoke them.

The login also needs the `org:tokens` scope, which isn't part of the CLI's
default login. The first `sentry token` command asks you to re-authorize and
adds it. Creating a token also needs `org:ci`, because a login can only give a
Comment thread
sentry-junior[bot] marked this conversation as resolved.
Resolved
new token scopes it holds itself, and revoking one needs `org:write` (org
owners and managers).

Re-authorization only happens in interactive runs: with `--yes`, `--json`, or
`--dry-run` the CLI never starts the authorization flow, so run a
`sentry token` command once without those flags first.

New tokens always get the `org:ci` scope. Sentry returns a token's value only
when it's created, so save it right away.

Self-hosted Sentry needs a version that supports the `org:tokens` scope.
Older servers reject the authorization request with an unknown-scope error.
2 changes: 1 addition & 1 deletion packages/cli/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ Top-level layout:
`dashboard`, `debug-files`, `docs`, `dsn`, `event`, `feedback`, `integration`, `issue`, `local`,
`log`, `monitor`, `org`, `platform`, `proguard`, `project`, `react-native`,
`release`, `replay`, `repo`, `snapshots`, `sourcemap`, `span`, `status`,
`team`, `trace`, `trial`) plus standalone command files (`api.ts`,
`team`, `token`, `trace`, `trial`) plus standalone command files (`api.ts`,
`explore.ts`, `help.ts`, `info.ts`, `init.ts`, `mcp.ts`, `schema.ts`,
`wasm-split.ts`, plus the hidden `bash-hook.ts`, `send-event.ts`, and
`send-envelope.ts`).
Expand Down
2 changes: 2 additions & 0 deletions packages/cli/lint-rules/cli-oxlint-plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@ const BOUND_TARGET_TYPES = new Set([
"ReplayArguments",
"SpanListArguments",
"SpanViewArguments",
"TokenArguments",
"TokenCreateTarget",
"TraceLogsArguments",
"TraceViewArguments",
"TrialStartArguments",
Expand Down
12 changes: 11 additions & 1 deletion packages/cli/plugins/sentry-cli/skills/sentry-cli/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ The `sentry` CLI follows conventions from well-known tools — if you're familia

### Safety Rules

- Always confirm with the user before running destructive commands: `alert issues delete`, `alert metrics delete`, `cli uninstall`, `dashboard widget delete`, `integration delete`, `issue merge`, `issue unlink`, `project delete`, `release delete`, `trial start`
- Always confirm with the user before running destructive commands: `alert issues delete`, `alert metrics delete`, `cli uninstall`, `dashboard widget delete`, `integration delete`, `issue merge`, `issue unlink`, `project delete`, `release delete`, `token delete`, `trial start`
- For mutations, verify the org/project context looks correct in the command output before proceeding with further changes
- Never store or log authentication tokens — the CLI manages credentials automatically
- If the CLI reports the wrong org/project, override with explicit `<org>/<project>` arguments
Expand Down Expand Up @@ -652,6 +652,16 @@ Work with Sentry teams

→ Full flags and examples: `references/team.md`

### Token

Manage org auth tokens

- `sentry token list [<org>]` — List org auth tokens
- `sentry token create <org/name>` — Create an org auth token
- `sentry token delete [<org>/]<token-id>` — Revoke an org auth token

→ Full flags and examples: `references/token.md`

### Integration

Work with Sentry integrations
Expand Down
106 changes: 106 additions & 0 deletions packages/cli/plugins/sentry-cli/skills/sentry-cli/references/token.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
---
name: sentry-cli-token
version: 0.48.0-dev.0
description: Manage org auth tokens
requires:
bins: ["sentry"]
auth: true
---

# Token Commands

Manage org auth tokens

### `sentry token list [<org>]`

List org auth tokens

**Arguments:**

| Argument | Description |
|----------|-------------|
| `[<org>]` | Organization slug (auto-detected if omitted) |

**JSON Fields** (use `--json --fields` to select specific fields):

| Field | Type | Description |
|-------|------|-------------|
| `id` | string | Token ID |
| `name` | string | Token name |
| `scopes` | array | Scopes granted to the token |
| `tokenLastCharacters` | string \| null | Last 4 characters of the token |
| `dateCreated` | string | Creation timestamp |
| `dateLastUsed` | string \| null | Last time the token was used, if ever |
| `projectLastUsedId` | string \| null | ID of the project the token was last used for |
| `token` | string | Full token value (create response only) |

**Examples:**

```bash
# List tokens (auto-detect org)
sentry token list

# List tokens in a specific org
sentry token list my-org

# Output as JSON
sentry token list --json
```

### `sentry token create <org/name>`

Create an org auth token

**Arguments:**

| Argument | Description |
|----------|-------------|
| `<org/name>` | [&lt;org&gt;/]&lt;name&gt; - Name of the new token |

**Flags:**
- `-n, --dry-run - Show what would happen without making changes`

**Examples:**

```bash
# Create a token (auto-detect org)
sentry token create github-actions

# Create a token in a specific org
sentry token create my-org/github-actions

# Output as JSON, including the token value
sentry token create my-org/github-actions --json
```

### `sentry token delete [<org>/]<token-id>`

Revoke an org auth token

**Destructive:** This command deletes data or makes changes that can't easily be undone. Use `--dry-run` to preview the changes first. Confirm with the user before running it.

**Arguments:**

| Argument | Description |
|----------|-------------|
| `[<org>/]<token-id>` | [&lt;org&gt;/]&lt;token-id&gt; - Token to revoke |

**Flags:**
- `-y, --yes - Skip confirmation prompt`
- `-f, --force - Force the operation without confirmation`
- `-n, --dry-run - Show what would happen without making changes`

**Examples:**

```bash
# Revoke a token (auto-detect org)
sentry token delete 12345

# Preview revoking a token
sentry token delete my-org/12345 --dry-run

# Revoke without the confirmation prompt
sentry token delete my-org/12345 --yes
```

All commands also support `--json`, `--fields`, `--help`, `--log-level`, and `--verbose` flags.
6 changes: 6 additions & 0 deletions packages/cli/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ import { listCommand as spanListCommand } from "./commands/span/list.js";
import { statusRoute } from "./commands/status/index.js";
import { teamRoute } from "./commands/team/index.js";
import { listCommand as teamListCommand } from "./commands/team/list.js";
import { tokenRoute } from "./commands/token/index.js";
import { listCommand as tokenListCommand } from "./commands/token/list.js";
import { traceRoute } from "./commands/trace/index.js";
import { listCommand as traceListCommand } from "./commands/trace/list.js";
import { trialRoute } from "./commands/trial/index.js";
Expand Down Expand Up @@ -112,6 +114,7 @@ const PLURAL_TO_SINGULAR: Record<string, string> = {
releases: "release",
repos: "repo",
teams: "team",
tokens: "token",
logs: "log",
monitors: "monitor",
replays: "replay",
Expand Down Expand Up @@ -146,6 +149,7 @@ export const routes = buildRouteMap({
release: releaseRoute,
repo: repoRoute,
team: teamRoute,
token: tokenRoute,
integration: integrationRoute,
issue: issueRoute,
event: eventRoute,
Expand Down Expand Up @@ -185,6 +189,7 @@ export const routes = buildRouteMap({
releases: releaseListCommand,
repos: repoListCommand,
teams: teamListCommand,
tokens: tokenListCommand,
logs: logListCommand,
monitors: monitorListCommand,
spans: spanListCommand,
Expand Down Expand Up @@ -213,6 +218,7 @@ export const routes = buildRouteMap({
releases: true,
repos: true,
teams: true,
tokens: true,
logs: true,
monitors: true,
spans: true,
Expand Down
159 changes: 159 additions & 0 deletions packages/cli/src/commands/token/create.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
/**
* sentry token create
*
* Create an org auth token (`org:ci`) and print its value. Sentry only
* returns the value in the create response.
*/

import type { SentryContext } from "../../context.js";
import { createOrgAuthToken } from "../../lib/api-client.js";
import { buildCommand } from "../../lib/command.js";
import { ORG_TARGET } from "../../lib/completion-target.js";
import { selectAccountForOrg } from "../../lib/db/auth.js";
import {
escapeMarkdownInline,
mdKvTable,
renderMarkdown,
safeCodeSpan,
} from "../../lib/formatters/markdown.js";
import { CommandOutput } from "../../lib/formatters/output.js";
import { DRY_RUN_ALIASES, DRY_RUN_FLAG } from "../../lib/mutate-command.js";
import {
type BoundPositional,
boundPositional,
} from "../../lib/positional-parse.js";
import { resolveOrgInput } from "../../lib/target-positional.js";
import type { OrgAuthToken } from "../../types/index.js";
import {
CREATE_SCOPES,
ensureTokenScopes,
explainTokenForbidden,
parseOrgScopedArg,
} from "./shared.js";

const USAGE_HINT = "sentry token create [<org>/]<name>";

type CreateFlags = {
readonly "dry-run": boolean;
readonly json: boolean;
readonly fields?: string[];
};

type CreateResult =
| { dryRun: true; org: string; name: string }
| (OrgAuthToken & { dryRun?: false; org: string });

/** A token name bound to the organization where it will be created. */
type TokenCreateTarget = BoundPositional<
ReturnType<typeof parseOrgScopedArg>,
{ readonly name: string; readonly org: string }
>;

function formatTokenCreated(result: CreateResult): string {
if (result.dryRun) {
return renderMarkdown(
`Would create org auth token ${safeCodeSpan(result.name)} in **${escapeMarkdownInline(result.org)}**. (dry run)`,
);
}
const rows: [string, string][] = [
["Name", escapeMarkdownInline(result.name)],
["ID", result.id],
["Scopes", result.scopes.join(", ")],
];
// Keep the one-time secret outside the table, which hard-wraps long cells.
const token = result.token
? `\n\nToken:\n\n${safeCodeSpan(result.token)}`
: "";
return renderMarkdown(
`## Org auth token created in ${escapeMarkdownInline(result.org)}\n\n${mdKvTable(rows)}${token}`,
);
}

export const createCommand = buildCommand({
docs: {
brief: "Create an org auth token",
fullDescription:
"Create an org auth token with the org:ci scope, for uploading source maps, " +
"debug files, and releases from CI.\n\n" +
"The token value is printed once. Sentry doesn't store it, so save it now.\n\n" +
"Use `org/name` to pick the org: everything before the first `/` is the org " +
"slug. Add the org when the name itself contains a `/`.\n\n" +
"Needs a CLI login (`sentry auth login`). The first run asks you to " +
"re-authorize so the login can manage org auth tokens (interactive runs " +
"only; skipped with --json).",
examples: [
{
description: "Create a token (auto-detect org)",
command: "sentry token create github-actions",
},
{
description: "Create a token in a specific org",
command: "sentry token create my-org/github-actions",
},
{
description: "Output as JSON, including the token value",
command: "sentry token create my-org/github-actions --json",
},
],
},
output: {
human: formatTokenCreated,
},
parameters: {
positional: {
kind: "tuple",
parameters: [
{
placeholder: "org/name",
brief: "[<org>/]<name> - Name of the new token",
parse: boundPositional(
ORG_TARGET,
(raw) => parseOrgScopedArg(raw, "Token name", USAGE_HINT),
async ({ value: name, orgSlug }, context: SentryContext) => {
const { org } = await resolveOrgInput(
{ org: orgSlug },
context.cwd,
USAGE_HINT,
);
return { name, org };
},
),
},
],
},
flags: {
"dry-run": DRY_RUN_FLAG,
},
aliases: { ...DRY_RUN_ALIASES },
},
async *func(
this: SentryContext,
flags: CreateFlags,
target: TokenCreateTarget,
) {
const { name, org } = await target.resolved;

if (flags["dry-run"]) {
yield new CommandOutput<CreateResult>({ dryRun: true, org, name });
return { hint: "Dry run — no token was created." };
}

selectAccountForOrg(org);
const scopeMissing = await ensureTokenScopes(
CREATE_SCOPES,
"Creating an org auth token",
);

let token: OrgAuthToken;
try {
token = await createOrgAuthToken(org, name);
} catch (error) {
throw explainTokenForbidden(error, CREATE_SCOPES, scopeMissing);
}

yield new CommandOutput<CreateResult>({ ...token, org });
return {
hint: "Save the token now. Sentry can't show it again.",
};
},
});
Loading
Loading