Skip to content
Open
Show file tree
Hide file tree
Changes from 6 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
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,16 @@ adheres to [Semantic Versioning](http://semver.org/).

## [Unreleased]

### Added

- Add `hb auth login` for OAuth sign-in as an alternative to personal auth
tokens, plus `hb auth status` and `hb auth logout`. Login picks the best
method automatically — the browser-based authorization code flow with PKCE
(RFC 8252/7636) locally, or the device authorization flow (RFC 8628) in
SSH/headless environments — and `--web`/`--device` force a method. Tokens
are stored in `~/.honeybadger-cli-credentials.json` and refreshed
automatically.

## [0.9.0] - 2026-07-17

### Added
Expand Down
41 changes: 40 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,45 @@ Run with `--help` for all options including `--version`, `--interval`, and `--in

The CLI can be configured using either command-line flags, environment variables, or a configuration file.

### Authentication

The CLI talks to two Honeybadger APIs with different credentials:

* **Reporting API** (`deploy`, `agent`, `run`, `check-in`): uses a project API key via `--api-key` or `HONEYBADGER_API_KEY`.
* **Data API** (everything else): uses your personal credentials — either OAuth via `hb auth login`, or a personal auth token.

The easiest way to authenticate the Data API commands is OAuth:

```bash
hb auth login # sign in (picks the best method for your environment)
hb auth login --web # force the browser flow on this machine
hb auth login --device # force the device flow (sign in from another device)
hb auth status # show how you're currently authenticated
hb auth logout # revoke and delete the stored credentials
```

`hb auth login` picks the sign-in method automatically: on a machine with a
browser it uses the OAuth 2.0 authorization code flow with PKCE (RFC 8252); in
an SSH session or on a headless machine it uses the device authorization flow
(RFC 8628, where the server supports it), showing a one-time code you enter
from any other device. Either way it prints a tip for switching to the other
method. It authenticates against the endpoint you've configured (US by
default, EU with `--endpoint https://eu-api.honeybadger.io`). Tokens are
stored in
`~/.honeybadger-cli-credentials.json` (owner-only permissions; override the
location with `HONEYBADGER_CREDENTIALS_FILE`) and are refreshed automatically
when they expire.

Alternatively, set a personal auth token with `--auth-token`,
`HONEYBADGER_AUTH_TOKEN`, or `auth_token` in the config file. A personal auth
token always takes precedence over stored OAuth credentials.

**Note:** OAuth tokens are scoped to the account you choose on the consent
screen. Commands that span accounts — notably `hb accounts list` — return
`403 Insufficient scope` with an OAuth token; use a personal auth token for
those. Account-scoped commands (projects, faults, insights, and so on) work
normally with either.

### Configuration File

By default, the CLI looks for a configuration file at `~/.honeybadger-cli.yaml` in your home directory. You can specify a different configuration file using the `--config` flag.
Expand Down Expand Up @@ -101,7 +140,7 @@ These commands use `--api-key` or `HONEYBADGER_API_KEY` (project API key):

### Data API Commands

These commands use `--auth-token` or `HONEYBADGER_AUTH_TOKEN` (personal auth token):
These commands authenticate with `hb auth login` (OAuth), or with `--auth-token` / `HONEYBADGER_AUTH_TOKEN` (personal auth token):

| Command | Description |
|---------|-------------|
Expand Down
159 changes: 35 additions & 124 deletions cmd/accounts.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import (

hbapi "github.com/honeybadger-io/api-go"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)

var (
Expand All @@ -35,19 +34,11 @@ var accountsListCmd = &cobra.Command{
Short: "List all accounts",
Long: `List all accounts accessible with your auth token.`,
RunE: func(_ *cobra.Command, _ []string) error {
authToken := viper.GetString("auth_token")
if authToken == "" {
return fmt.Errorf(
"auth token is required. Set it using --auth-token flag or HONEYBADGER_AUTH_TOKEN environment variable",
)
client, err := newDataAPIClient()
if err != nil {
return err
}

endpoint := convertEndpointForDataAPI(viper.GetString("endpoint"))

client := hbapi.NewClient().
WithBaseURL(endpoint).
WithAuthToken(authToken)

ctx := context.Background()
accounts, err := client.Accounts.List(ctx)
if err != nil {
Expand Down Expand Up @@ -87,19 +78,11 @@ var accountsGetCmd = &cobra.Command{
return fmt.Errorf("account ID is required. Set it using --id flag")
}

authToken := viper.GetString("auth_token")
if authToken == "" {
return fmt.Errorf(
"auth token is required. Set it using --auth-token flag or HONEYBADGER_AUTH_TOKEN environment variable",
)
client, err := newDataAPIClient()
if err != nil {
return err
}

endpoint := convertEndpointForDataAPI(viper.GetString("endpoint"))

client := hbapi.NewClient().
WithBaseURL(endpoint).
WithAuthToken(authToken)

ctx := context.Background()
account, err := client.Accounts.Get(ctx, accountID)
if err != nil {
Expand Down Expand Up @@ -143,19 +126,11 @@ var accountsUsersListCmd = &cobra.Command{
return fmt.Errorf("account ID is required. Set it using --account-id flag")
}

authToken := viper.GetString("auth_token")
if authToken == "" {
return fmt.Errorf(
"auth token is required. Set it using --auth-token flag or HONEYBADGER_AUTH_TOKEN environment variable",
)
client, err := newDataAPIClient()
if err != nil {
return err
}

endpoint := convertEndpointForDataAPI(viper.GetString("endpoint"))

client := hbapi.NewClient().
WithBaseURL(endpoint).
WithAuthToken(authToken)

ctx := context.Background()
users, err := client.Accounts.ListUsers(ctx, accountID)
if err != nil {
Expand Down Expand Up @@ -199,19 +174,11 @@ var accountsUsersGetCmd = &cobra.Command{
return fmt.Errorf("user ID is required. Set it using --user-id flag")
}

authToken := viper.GetString("auth_token")
if authToken == "" {
return fmt.Errorf(
"auth token is required. Set it using --auth-token flag or HONEYBADGER_AUTH_TOKEN environment variable",
)
client, err := newDataAPIClient()
if err != nil {
return err
}

endpoint := convertEndpointForDataAPI(viper.GetString("endpoint"))

client := hbapi.NewClient().
WithBaseURL(endpoint).
WithAuthToken(authToken)

ctx := context.Background()
user, err := client.Accounts.GetUser(ctx, accountID, accountUserID)
if err != nil {
Expand Down Expand Up @@ -253,19 +220,11 @@ var accountsUsersUpdateCmd = &cobra.Command{
return fmt.Errorf("role is required. Set it using --role flag")
}

authToken := viper.GetString("auth_token")
if authToken == "" {
return fmt.Errorf(
"auth token is required. Set it using --auth-token flag or HONEYBADGER_AUTH_TOKEN environment variable",
)
client, err := newDataAPIClient()
if err != nil {
return err
}

endpoint := convertEndpointForDataAPI(viper.GetString("endpoint"))

client := hbapi.NewClient().
WithBaseURL(endpoint).
WithAuthToken(authToken)

ctx := context.Background()
if err := client.Accounts.UpdateUser(
ctx,
Expand Down Expand Up @@ -312,21 +271,13 @@ var accountsUsersRemoveCmd = &cobra.Command{
return fmt.Errorf("user ID is required. Set it using --user-id flag")
}

authToken := viper.GetString("auth_token")
if authToken == "" {
return fmt.Errorf(
"auth token is required. Set it using --auth-token flag or HONEYBADGER_AUTH_TOKEN environment variable",
)
client, err := newDataAPIClient()
if err != nil {
return err
}

endpoint := convertEndpointForDataAPI(viper.GetString("endpoint"))

client := hbapi.NewClient().
WithBaseURL(endpoint).
WithAuthToken(authToken)

ctx := context.Background()
err := client.Accounts.RemoveUser(ctx, accountID, accountUserID)
err = client.Accounts.RemoveUser(ctx, accountID, accountUserID)
if err != nil {
return fmt.Errorf("failed to remove user: %w", err)
}
Expand All @@ -353,19 +304,11 @@ var accountsInvitationsListCmd = &cobra.Command{
return fmt.Errorf("account ID is required. Set it using --account-id flag")
}

authToken := viper.GetString("auth_token")
if authToken == "" {
return fmt.Errorf(
"auth token is required. Set it using --auth-token flag or HONEYBADGER_AUTH_TOKEN environment variable",
)
client, err := newDataAPIClient()
if err != nil {
return err
}

endpoint := convertEndpointForDataAPI(viper.GetString("endpoint"))

client := hbapi.NewClient().
WithBaseURL(endpoint).
WithAuthToken(authToken)

ctx := context.Background()
invitations, err := client.Accounts.ListInvitations(ctx, accountID)
if err != nil {
Expand Down Expand Up @@ -414,19 +357,11 @@ var accountsInvitationsGetCmd = &cobra.Command{
return fmt.Errorf("invitation ID is required. Set it using --invitation-id flag")
}

authToken := viper.GetString("auth_token")
if authToken == "" {
return fmt.Errorf(
"auth token is required. Set it using --auth-token flag or HONEYBADGER_AUTH_TOKEN environment variable",
)
client, err := newDataAPIClient()
if err != nil {
return err
}

endpoint := convertEndpointForDataAPI(viper.GetString("endpoint"))

client := hbapi.NewClient().
WithBaseURL(endpoint).
WithAuthToken(authToken)

ctx := context.Background()
invitation, err := client.Accounts.GetInvitation(ctx, accountID, accountInvitationID)
if err != nil {
Expand Down Expand Up @@ -483,19 +418,11 @@ Example JSON payload:
return fmt.Errorf("JSON payload is required. Set it using --cli-input-json flag")
}

authToken := viper.GetString("auth_token")
if authToken == "" {
return fmt.Errorf(
"auth token is required. Set it using --auth-token flag or HONEYBADGER_AUTH_TOKEN environment variable",
)
client, err := newDataAPIClient()
if err != nil {
return err
}

endpoint := convertEndpointForDataAPI(viper.GetString("endpoint"))

client := hbapi.NewClient().
WithBaseURL(endpoint).
WithAuthToken(authToken)

jsonData, err := readJSONInput(accountCLIInputJSON)
if err != nil {
return fmt.Errorf("failed to read JSON input: %w", err)
Expand Down Expand Up @@ -559,19 +486,11 @@ Example JSON payload:
return fmt.Errorf("JSON payload is required. Set it using --cli-input-json flag")
}

authToken := viper.GetString("auth_token")
if authToken == "" {
return fmt.Errorf(
"auth token is required. Set it using --auth-token flag or HONEYBADGER_AUTH_TOKEN environment variable",
)
client, err := newDataAPIClient()
if err != nil {
return err
}

endpoint := convertEndpointForDataAPI(viper.GetString("endpoint"))

client := hbapi.NewClient().
WithBaseURL(endpoint).
WithAuthToken(authToken)

jsonData, err := readJSONInput(accountCLIInputJSON)
if err != nil {
return fmt.Errorf("failed to read JSON input: %w", err)
Expand Down Expand Up @@ -630,21 +549,13 @@ var accountsInvitationsDeleteCmd = &cobra.Command{
return fmt.Errorf("invitation ID is required. Set it using --invitation-id flag")
}

authToken := viper.GetString("auth_token")
if authToken == "" {
return fmt.Errorf(
"auth token is required. Set it using --auth-token flag or HONEYBADGER_AUTH_TOKEN environment variable",
)
client, err := newDataAPIClient()
if err != nil {
return err
}

endpoint := convertEndpointForDataAPI(viper.GetString("endpoint"))

client := hbapi.NewClient().
WithBaseURL(endpoint).
WithAuthToken(authToken)

ctx := context.Background()
err := client.Accounts.DeleteInvitation(ctx, accountID, accountInvitationID)
err = client.Accounts.DeleteInvitation(ctx, accountID, accountInvitationID)
if err != nil {
return fmt.Errorf("failed to delete invitation: %w", err)
}
Expand Down
Loading
Loading