Repository: cli/cli

Issue:
`gh auth status` should have a JSON output option
### Describe the feature or problem you’d like to solve

There isn't a good way to easily integrate tools with the `gh` command to use the token. For example to create a Docker credential helper like in #5150 because the current output of `gh auth status` is formatted for human viewing and isn't guaranteed to remain stable.

### Proposed solution

The command `gh auth status` should have an optional JSON output option to allow for integration with other tools that might need to use a Github token.

You can get just the token with `gh auth token` but it could be helpful to have other information such as the username, and the scopes. That way if necessary a command could run a `gh auth refresh --scopes <scope>` if the appropriate scope is not available. So for example a Docker credential helper as mentioned above could check if the token has the "read:packages" or even "write:packages" scope if necessary to push.

### Additional context

An example output could be something like
```json
{
    "github.com": {
        "active": true,
        "token": "gho_*******",
        "scopes": [
            "gist",
            "repo",
            "write:packages"
        ]
    }
}
```


Hints from issue comments:
@williammartin: Hey @evilhamsterman thanks for opening this enhancement request. It generally seems sensible to me though I'm curious if you have a _specific_ need as opposed to a theorised one around docker credential managers? I'm not at all opposed to the idea but I would like the design to be motivated by some real need, if that makes sense. Cheers!@evilhamsterman: @williammartin it isn't a theorized one, #5150 has been open for a while without any apparent movement. I was looking to go ahead and build one as my company is looking to use ghcr for internal containers and other packages.

Another example in my company, I have written scripts to help administer various aspects of our GitHub organizations. I'd prefer to grab the token from gh than have them generate and manage PATs, or setup my own GitHub app. We already use gh and takes care of storing things rather than having to reinvent the wheel for small scripts.

I can get the token from gh auth token, but I also would like to be able to check that the token has the appropriate scopes for the appropriate orgs. I can do that by either trying to parse the existing output, or by making a general API call and inspecting the headers returned, but it would be much easier and quicker to just call the command, get a json object to parse and grab the values needed.@williammartin: @evilhamsterman, sure I'm familiar with that issue. My point was less "is a docker credential helper needed" and more "is this going to help someone build a docker credential helper right now" because having a real user of this change is important both for design and prioritisation. It sounds like if we added support for this it would help you right now.

It generally sounds like a reasonable idea to me, I'm just gonna tag @andyfeller on here in case he has any additional thoughts.@evilhamsterman: Thanks it'll also make my life easier for work@vedosis: Here's a real world example:

https://xbarapp.com/docs/plugins/Dev/GitHub/pull-requests.5m.js.html

Being able to use automation to:
1. fetch your current tokens (rather than hard-coding the values)
2. ping a GitHub API with a specific query + tokens
3. send off a notification or flash your IoT lights 🤷🏻 @andyfeller: @evilhamsterman, @williammartin: Thank you for your patience catching up! 🙇  My main thought stems from a security perspective, which I think is minimized as I elaborate below.

### Security concerns 🤔 

My main concern, which I believe is abated, relates to ["Security hardening for GitHub Actions"](https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions#using-secrets) given ["GitHub CLI use also in GitHub Actions workflows"](https://docs.github.com/en/actions/using-workflows/using-github-cli-in-workflows):

> **Never use structured data as a secret**
> 
> Structured data can cause secret redaction within logs to fail, because redaction largely relies on finding an exact match for the specific secret value. For example, do not use a blob of JSON, XML, or YAML (or similar) to encapsulate a secret value, as this significantly reduces the probability the secrets will be properly redacted. Instead, create individual secrets for each sensitive value.

In the case of GitHub Actions, this required the person who created a workflow or custom action to intentionally know how the secrets used worked and handling explicitly masking them.  Knowing it might be complicated if the GitHub CLI had to emit the necessary [`::add-mask` workflow commands](https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#masking-a-value-in-a-log), I got confirmation from our `actions` friends that all of the [known GitHub token formats](https://github.blog/2021-04-05-behind-githubs-new-authentication-token-formats/) are automatically masked as secrets without further intervention.

My secondary thought is whether we should intentionally disallow the `--jq` support for this command. 🤔  

After saying that, I realize practically there is nothing to stop a user from manipulating the output of `gh auth status --json` externally with `jq` or other tools, so it would merely be a hindrance with no real security.@williammartin: In an internal conversation I mentioned that I believe `--show-token` should still be required to remove the token mask even if `--json` is provided.@evilhamsterman: @andyfeller I appreciate the security concern, but I also feel that it isn't always you job to prevent a useful function that could improve security in several ways because of a concern for what could be an occasional issue in other situations. This feels like a "but sometimes" of software, https://youtu.be/GiYO1TObNz8.

In this case I think the the ability to more easily use `gh` to manage tokens in personal scripts vs something like generating and storing a PAT far out weighs the concern of use in something like GitHub Actions where most people will be using `GITHUB_TOKEN` directly rather than passing it to `gh` just to get it again from `gh auth status --json`. Plus as you said it should be masked automatically anyway. If it is really a major concern in somewhat I can't see you could disable the flag when running in an Action workflow. You can still get the token with `gh auth token` or `gh auth status --show-token` this is just to help scripts by also providing a format that's easy to parse not just the token but the features like the token scopes by machine. 

I'm not sure how limiting the `--jq` functionality would create any more security concerns than is already present in just showing the token. It's just a way to easily pick the data you want from the output. Especially since like you said there isn't anything stopping you from piping it through `jq` or `yq` yourself. In fact it could improve security because it would be easier to have `gh` output just the scopes if that's what you want.

@williammartin I can see the value in including `--show-token`  without `--json`,  but I personally think that if you are requesting a json output with `--json`, that already implies you have an idea of what you're doing and you want all the values so `--show-token` shouldn't be necessary. However that's a minor quibble and would not be a blocker.

Ultimately all the values that you would get from a `--json` flag are available already through other methods, really this is just a nice to have that enables easier integration of `gh` with other scripts and tools to securely manage your GitHub token. @williammartin: > I can see the value in including --show-token without --json, but I personally think that if you are requesting a json output with --json, that already implies you have an idea of what you're doing and you want all the values so --show-token shouldn't be necessary. However that's a minor quibble and would not be a blocker.

I don't want people to accidentally expose their token while exploring CLI behaviour.

--

In any case it sounds like we're all on board there is value in this, and we have a general picture of how it will work. I think the only outstanding piece is to design for the failure cases to be useful.@jessehouwing: I too have this need. I have a number of scripts I use to admin our enterprise and try not to be logged in with a `admin:enterprise` level token all the time for security reasons. I also don't want to go through web-auth and SSO approval of the token on every script run.

So having the ability to check the available token scoped (or a separate command that would allow me to test which scopes I have) would be quite handy.

What I'm missing from the output is which organizations the token has been authorized against, cause that would also be a reason to require a re-auth.

As for concerns around the token itself, for me it could be omitted from the result altogether.

There's already a `gh auth token` command that will happily dump the token to the console without qualm.

Alternatively `gh` could detect it's running in Actions and issue the `add-mask` to the error stream while dumping the json to stdout. I think that should work ;).@williammartin: I'm labelling this `help wanted` because I believe it's a good thing to have, it has a few reactions and https://github.com/cli/cli/issues/9326 was just opened as well.

I'd ask that anyone who picks this up also works with us to design carefully about how we represent the different states, actions that are suggested and exit codes. Some ideas are in https://github.com/cli/cli/issues/9326. We can also start with a subset that would go a long way to satisfying the majority of use cases.@benjlevesque: @williammartin I'd be happy to contribute to this.
Regarding your comment 
> design carefully about how we represent the different states, actions that are suggested and exit codes

Here are my design ideas: 
### State
- states: add a `status`  field, which can be one of "success", "error". 
- In case of `error`, add an `error_reason` field, which can be `timeout` or `invalid_token` and an `error_message` which could be the same message as the normal output (ie. "Timeout trying to log in [...]")
-  `You are not logged into [...]` should result in an empty object `{}`?

### Exit code
Expected behaviors like Success, No account, or Invalid Token should result in a 0 code. 
Timeout or other unexpected errors should result in a non-0 code (always 1?)

Optionally, we could add an extra flag (something like  `--expect-success`) to change this behavior and only exit 0 in case of success.

### Suggested actions
I feel that the JSON format is intended for automation, and does not require providing "help" as the regular output does. Hence I would omit the suggested actions (`gh auth login -h INSTANCE_NAME`)


### Token protection
The token should remain hidden unless specified otherwise (`--show-token`)

### JSON Structure
#### Option 1: flat array
```json
[
  {
    "status": "success",
    "active": true,
    "host": "github.com",
    "user": "USERNAME",
    "token": "gho_************************************",
    "token_source": "keyring",
    "git_protocol": "ssh",
    "scopes": "admin:org, gist, repo"
  },
  {
    "status": "error",
    "host": "ghe.io",
    "user": "USERNAME",
    "token_source": "GH_CONFIG_DIR/hosts.yml",
    "git_protocol": "https",
    "error_reason": "timeout",
    "error_message": "Timeout trying to log in to ghe.io account USERNAME (GH_CONFIG_DIR/hosts.yml)"
  }
]
```
#### Option 2: map with host
```json
{
  "github.com": [{
     // ...
  }],
  "ghe.io": [{
     // ...
  }]
}
```

Option 1 is my preferred one because the hostname at the top feels redundant; it seems easier to manipulate an array, especially since the command offers a `--hostname` flag which makes filtering easy. An extra `--username` flag could be added to simplify further. @briceburg: I like the implementation detailed in https://github.com/cli/cli/issues/8637#issuecomment-2246318117  -- having an -exit-code would help greatly to quickly determine if the `gh` cli has active credentials, configured correctly. 

My immediate use case is as part of an [interactive] developer bootstrap utility. The bootstrap needs to know if `gh` has been properly configured/authenticated so that it can make subsequent API calls... and if it has not, then the script could call `gh auth login` + `gh auth git-config` as an example. @evilhamsterman: Been a while since any activity on this it seems. I may take a stab at it. Personally I prefer the mapping format, as I suggested in my initial post. I'm not sure how it's easier to manipulate an array vs a map especially since the point of this is data retrieval not manipulation. You can filter with `--hostname` but perhaps you do want all and want to parse them somehow, using an array would mean you have to loop through all of them to collect everything, having a map would let you easily parse just the respective hosts. Having the host as the key doesn't have to be redundant, just omit it from the status object, or keep both and people can ignore it.

Expanding on my previous suggestion with @benjlevesque's additions

```json
{
    "github.com": [
        {
            "status": "success",
            "active": true,
            "host": "github.com",
            "user": "USERNAME",
            "token": "gho_************************************",
            "token_source": "keyring",
            "git_protocol": "ssh",
            "scopes": "admin:org, gist, repo",
        },
        {
            "status": "error",
            "host": "github.com",
            "user": "USERNAME",
            "token_source": "GH_CONFIG_DIR/hosts.yml",
            "git_protocol": "https",
            "error_reason": "invalid",
            "error_message": "Invalid token you must reauthenticate"
        }
    ],
    "ghe.io": [
        {
            "status": "success",
            "active": false,
            "host": "ghe.io",
            "user": "USERNAME",
            "token_source": "GH_CONFIG_DIR/hosts.yml",
            "git_protocol": "https",
            "error_reason": "timeout",
            "error_message": "Timeout trying to log in to ghe.io account USERNAME (GH_CONFIG_DIR/hosts.yml)"
        }
    ]
}
```

As to exit codes, I'd go ahead an always exit 0 unless the we can't return anything at all like if there are no accounts, or the `--hostname` doesn't exist. To me it doesn't make sense to exit with an error because one account out of several has an issue when the other's are fine. A lot of languages have an option to check if a called process exited with an error, example Python's `subprocess.popopen(check=True)`.

Too me it seems simpler to only throw an error if the request completely fails, then I can handle that path. If it at least partially succeeds then I can just check `hostname[].account.status==success`@BagToad: @babakks will write AC for this.@babakks: Sorry for the long comment here, but I think it's needed to shed light on small details here.

I'm now going through a quick break down of various scenarios and how `auth status` currently works. Then I'll dig into the machine-friendly output requirements and grab some ideas from #9326, and at the end, I'll share a *candidate A/C* so that we can talk about and polish it.

Please feel free to share your thoughts. 🍻

Also cc-ing @andyfeller and @BagToad.

## How `auth status` currently works

To recap, the `auth status` command iterates over all available tokens, including the `GH_TOKEN` env var, checks if they're still valid (by calling an API endpoint to retrieve their scopes), and build an itemised summary, each entry of which can be one of the following:
- a valid entry
- an invalid entry
- a timeout entry

A timeout entry, can be actually a valid/invalid one, but it's just `gh` couldn't confirm their status at the time of running.

Anyway, if there's at least one non-valid entry among the report, the command terminates with exit code 1.

But this is the happy path of `auth login`. For example, if there are no tokens available, or if then given `--hostname` option value was not among the known hosts, the command fails with exit code 1.

In other words, `auth status` exists with zero, only if all tokens are valid. **We strongly need to keep this behaviour intact to avoid breaking any usages.**

## Machine-friendly output

We're going to shape:
- a CLI experience (i.e. options and usage)
- a sensible exit code regime
- a JSON output structure

### 1. CLI experience

As I'm aware of, `gh` has two styles for enabling a JSON output:
1. **Combination of `--json <fields>`, `--jq <query>`, and `--template <template>` options,** like in `pr view` or `issue list`.
   Note that with the `--json` field it's required to provide the list of fields. I mean, you cannot just pass `--json` and expect the commands to retrieve all fields. The reason behind this design is that some fields require further API calls, which are sometimes expensive in terms of API burden and/or rate-limit capacity.
2. **Combination of `--format json`, `--jq <query>`, and `--template <template>` options,** like in `release verify`.

Based on this, I think the idiomatic way is to go with the second style, as there's no point/benefit in picking individual fields since they're readily available without further API calls. This is how a common use case of `auth status` would look like:

```sh
gh auth status --format json --jq '."github.com" | .[] | select (.state == "success") | .user'
```

### 2. Exit code

Sorry, but I disagree with @evilhamsterman in that we should only return with zero exit code. My personal reason is, as a user, I don't expect a command's exit code to be different based on a mere **formatting** option. Not saying there isn't such cases, just it's not a nice experience. And expectedly it'd be niche enough that users might miss it even if we clearly document it. I know, if I could go back in time, I would argue against the current behaviour, but I think we're tied to our path now.

Okay, if we're going to keep the exit codes as they're in the interactive flow, then we should note that whoever is calling `auth status` with the `--json` option, has all the rights to expect a valid JSON output regardless of the exit code. I personally hate to do a *try-parse* kind of thing.

So, this seems to be a legit regime in the machine-readable mode:
| When                                      | Exit code | stdout  | stderr                    |
| ----------------------------------------- | --------- | ------- | ------------------------- |
| no tokens available                       | 1         | `{}`    | *[current error message]* |
| no token available for given `--hostname` | 1         | `{}`    | *[current error message]* |
| one or more invalid/timeout entries       | 1         | `{...}` | [empty]                   |
| no invalid/timeout entries                | 0         | `{...}` | [empty]                   |

### 3. JSON structure

I agree with @williammartin that the `--show-token` option should still be respected. However, if the `--show-token` is not provided, it's obvious that showing a masked token in machine-readable output doesn't make any sense. So, the `token` field must only be present if the the `--show-token` is set.

I also think the map structure suggested by @evilhamsterman is more intuitive and straightforward to lookup for entries.

So, the JSON output can look like this (note the different types of entries):

```jsonc
{
    "hostname": [
        {
            // valid entry
            "state": "success",

            "active": true,               // or false
            "host": "hostname",           // redundant, but still spares holding parent key
            "user": "username",
            "tokenSource": "oauth_token", // or an env var name
            "token": "...",               // present only if `--show-token` is provided
            "scopes": "...",
            "gitProtocol": "ssh",         // or "https"
        },
        {
            // invalid entry
            "state": "error",
            "error": "[error message]",

            "active": false,              // or true
            "host": "hostname",
            "user": "username",
            "tokenSource": "oauth_token", // or an env var name
        },
        {
            // timeout entry
            "state": "timeout",
            "error": "[error message]",

            "active": false,              // or true
            "host": "hostname",
            "user": "username",
            "tokenSource": "oauth_token", // or an env var name
        },
    ],
    // ...
}
```

## Candidate A/C

### No tokens
**Given** I have no auth tokens on my host machine
**When** I run `gh auth status --format json`
**Then** I get an empty JSON on stdout (`{}`), and the message below on stderr, with exit code 1.

```
You are not logged into any GitHub hosts. To log in, run: gh auth login
```

### No token for the given `--hostname`
**Given** I have no auth tokens for a GitHub host named `foo.com` on my host machine
**When** I run `gh auth status --hostname foo.com --format json`
**Then** I get an empty JSON on stdout (`{}`), and the message below on stderr, with exit code 1.

```
You are not logged into any accounts on foo.com
```

### All valid tokens
**Given** I have valid auth tokens for one or more GitHub hosts (all valid tokens)
**When** I run `gh auth status --format json`
**Then** I get a JSON output like below (without token or its mask) with a **zero** exit code.

```jsonc
{
    "github.com": [
        {
            "state": "success",
            "active": true,
            "host": "github.com",
            "user": "my-username",
            "tokenSource": "...", 
            "scopes": "...",
            "gitProtocol": "ssh",
        },
        // other accounts...
    ],
    // other hosts...
}
```

### All valid tokens, with `--hostname`
**Given** I have valid auth tokens for one or more GitHub hosts (all valid tokens)
**When** I run `gh auth status --hostname github.com --format json`
**Then** I get a JSON output like below (without token or its mask) with a **zero** exit code. Note that there's only one hostname in the output.

```jsonc
{
    "github.com": [
        {
            "state": "success",
            "active": true,
            "host": "github.com",
            "user": "my-username",
            "tokenSource": "...", 
            "scopes": "...",
            "gitProtocol": "ssh",
        },
        // other accounts...
    ],
}
```

### All valid tokens, with `--show-token`
**Given** I have valid auth tokens for one or more GitHub hosts (all valid tokens)
**When** I run `gh auth status --show-token --format json`
**Then** I get a JSON output like below including the token as plaintext, with a **zero** exit code.

```jsonc
{
    "github.com": [
        {
            "state": "success",
            "active": true,
            "host": "github.com",
            "user": "my-username",
            "tokenSource": "...", 
            "token": "#################################",
            "scopes": "...",
            "gitProtocol": "ssh",
        },
        // other accounts...
    ],
    // other hosts...
}
```

### All valid tokens, with `--hostname` and `--show-token`
**Given** I have valid auth tokens for one or more GitHub hosts (all valid tokens)
**When** I run `gh auth status --hostname github.com --show-token --format json`
**Then** I get a JSON output like below including the token as plaintext, with a **zero** exit code. Note that there's only one hostname in the output.

```jsonc
{
    "github.com": [
        {
            "state": "success",
            "active": true,
            "host": "github.com",
            "user": "my-username",
            "tokenSource": "...", 
            "token": "#################################",
            "scopes": "...",
            "gitProtocol": "ssh",
        },
        // other accounts...
    ],
}
```

### An invalid token
**Given** I have an invalid auth token (among other tokens)
**When** I run `gh auth status --format json`
**Then** I get a JSON output like below with **exit code 1**.

```jsonc
{
    "github.com": [
        {
            "state": "error",
            "error": "[error message]",
            "active": true,
            "host": "github.com",
            "user": "my-username",
            "tokenSource": "...", 
        },
        // other accounts...
    ],
    // other hosts...
}
```

### An unreachable host (timeout)
**Given** I have a valid (or invalid, doesn't matter) auth token that for an unreachable GitHub host
**When** I run `gh auth status --format json`
**Then** I get a JSON output like below with **exit code 1**.

```jsonc
{
    "github.com": [
        {
            "state": "timeout",
            "error": "[error message]",
            "active": true,
            "host": "github.com",
            "user": "my-username",
            "tokenSource": "...", 
        },
        // other accounts...
    ],
    // other hosts...
}
```

### With `--jq`
**Given** I have auth tokens on my host machine
**When** I run `gh auth status --format json --jq <QUERY>`
**Then** I get the result of applying the given `<QUERY>` on the JSON data.

### With `--template`
**Given** I have auth tokens on my host machine
**When** I run `gh auth status --format json --template <TEMPLATE>`
**Then** I get the result of rendering the given `<TEMPLATE>` from the JSON data.
@evilhamsterman: @babakks that looks good. While I disagree on the exit code, that's my preference, it's also not a big problem to just ignore it when parsing and handle errors further up.

I haven't had much time to dig in , but earlier when poking around, I was thinking that a good course of action may be to have the command always essentially generate the json structure internally. Then the output is just formatting.

If you set `--format json` just dump the structure, with or without filtering for the token. If you don't then format it for the CLI output. 

That also opens up another option for people who want to use `gh` for authentication. If the auth values are split from the display logic and handled in its own module, then people writing Go could import the module directly in their own code.

It's a bit of a feature creep though. Just getting the json would be a big improvement @babakks: Thanks for your thoughts, @evilhamsterman!

Actually, the implementation is quite similar to what you just described. The only thing is to make the type implement the `Exportable` interface.

As for usage as a library, this module (i.e. `github.com/cli/cli/v2`) is not meant to be used as a dependency. That's why shared/common components are shipped via `github.com/cli/go-gh/v2`. But anyway, I get your point as an engineering practice to  reduce coupling among components and make them reusable.@BagToad: > Sorry, but I disagree with [@evilhamsterman](https://github.com/evilhamsterman) in that we should only return with zero exit code. My personal reason is, as a user, I don't expect a command's exit code to be different based on a mere **formatting** option. Not saying there isn't such cases, just it's not a nice experience. And expectedly it'd be niche enough that users might miss it even if we clearly document it. I know, if I could go back in time, I would argue against the current behaviour, but I think we're tied to our path now.

Do you know what the precedence is with our other command sets? I'm not passing an opinion here, but I'd like to understand where this proposed behavior lies with our other command sets' behavior.

Let's take `gh pr checks` as a single example. We exit 1 when we have at least one failed PR check.

<details><summary>example gh pr checks failure</summary>
<p>

```
❯ gh pr checks            
Some checks were not successful
0 cancelled, 1 failing, 10 successful, 0 skipped, and 0 pending checks

   NAME                                                               DESCRIPTION  ELAPSED  URL                                                             
X  Lint/govulncheck (pull_request)                                                 52s      https://github.com/cli/cli/actions/runs/16782852957/job/47525...
✓  CodeQL                                                                          2s       https://github.com/cli/cli/runs/47525970617
✓  Code Scanning/CodeQL-Build (actions) (pull_request)                             1m7s     https://github.com/cli/cli/actions/runs/16782852962/job/47525...
✓  Code Scanning/CodeQL-Build (go) (pull_request)                                  4m12s    https://github.com/cli/cli/actions/runs/16782852962/job/47525...
✓  Unit and Integration Tests/build (macos-latest) (pull_request)                  7m17s    https://github.com/cli/cli/actions/runs/16782852940/job/47525...
✓  Unit and Integration Tests/build (ubuntu-latest) (pull_request)                 5m22s    https://github.com/cli/cli/actions/runs/16782852940/job/47525...
✓  Unit and Integration Tests/build (windows-latest) (pull_request)                11m45s   https://github.com/cli/cli/actions/runs/16782852940/job/47525...
✓  Unit and Integration Tests/integration-tests (macos-latest) (p...               1m8s     https://github.com/cli/cli/actions/runs/16782852940/job/47525...
✓  Unit and Integration Tests/integration-tests (ubuntu-latest) (...               56s      https://github.com/cli/cli/actions/runs/16782852940/job/47525...
✓  Unit and Integration Tests/integration-tests (windows-latest) ...               5m13s    https://github.com/cli/cli/actions/runs/16782852940/job/47525...
✓  Lint/lint (pull_request)                                                        2m12s    https://github.com/cli/cli/actions/runs/16782852957/job/47525...

❯ echo $?
1

```

</p>
</details>

But, let's check the `--json`:

<details><summary>example gh pr checks with --json</summary>
<p>


```
❯ gh pr checks --json name
[
  {
    "name": "CodeQL"
  },
  {
    "name": "build (macos-latest)"
  },
  {
    "name": "integration-tests (macos-latest)"
  },
  {
    "name": "build (ubuntu-latest)"
  },
  {
    "name": "integration-tests (ubuntu-latest)"
  },
  {
    "name": "integration-tests (windows-latest)"
  },
  {
    "name": "CodeQL-Build (go)"
  },
  {
    "name": "lint"
  },
  {
    "name": "CodeQL-Build (actions)"
  },
  {
    "name": "build (windows-latest)"
  },
  {
    "name": "govulncheck"
  }
]

❯ echo $?                 
0

```

</p>
</details> 


As you can see, it exits 0.

If we choose to make `gh auth status`'s JSON output fail too, we are deviating from `gh pr checks`. I think `gh` users value behavior consistency between command sets, so I ask that we please make sure that this is definitely worth breaking convention before proceeding with it. 

And I also ask that other people give their knowledge of other command sets behavior because perhaps `gh pr checks` is an outlier and I'm just missing how other command sets do it. 


-----

> I agree with [@williammartin](https://github.com/williammartin) that the `--show-token` option should still be respected. However, if the `--show-token` is not provided, it's obvious that showing a masked token in machine-readable output doesn't make any sense. So, the `token` field must only be present if the the `--show-token` is set.

Can you talk more about this? I am actually under the impression that always using the same schema would be more valuable than conditionally removing `token` from the output schema.

In other words, I'd prefer if `token` was always in the output and that we obfuscate the value unless `--show-token` is called. 

Especially if we're not doing the typical `--json fields...` format and just doing `--format json`. I want the output schema to be fixed.

-----

> That also opens up another option for people who want to use `gh` for authentication. If the auth values are split from the display logic and handled in its own module, then people writing Go could import the module directly in their own code.

> As for usage as a library, this module (i.e. `github.com/cli/cli/v2`) is not meant to be used as a dependency. That's why shared/common components are shipped via `github.com/cli/go-gh/v2`. But anyway, I get your point as an engineering practice to reduce coupling among components and make them reusable.

@babakks @evilhamsterman To elaborate, in case it is useful, you can already use our [`auth` package in `cli/go-gh`](https://github.com/cli/go-gh/blob/a08820a13f257d6c5b4cb86d37db559ec6d14577/pkg/auth/auth.go#L31-L36) to help with producing authentication tokens. 
@andyfeller: My main feedback here is 1) **trying to stick with `--json <fields>` experience we use across virtually all commands** and 2) **use `login` instead of `user`**:

1. Consistent experience means no surprises for users
2. It reuses the existing `--json` logic to handle showing or not showing the token
3. It would reduce the number of flags for exporting the information from 2 to 1
4. I don't want to assume all sessions are tied to users but might be apps or bots, so the `login` verbiage would be general enough

Putting aside whether `host` is contained in a given host array entry, `--json active,gitProtocol,login,token,scopes,state` would allow users to choose what they export using the same experience as other commands without needing a separate flag for including tokens:

```json
{
  "github.com": [
    {
      "state": "success",
      "active": true,
      "host": "github.com",
      "user": "my-username",
      "tokenSource": "...", 
      "scopes": "...",
      "gitProtocol": "ssh",
    },
    // other accounts...
  ],
  // other hosts...
}
```

I don't think `--show-token` should really affect `--json` as someone would explicitly be asking for `token` field and should know it will be plaintext, $0.02

So essentially:

```shell
$ gh auth status --help
Display active account and authentication state on each known GitHub host.

For each host, the authentication state of each known account is tested and any issues are included in the output.
Each host section will indicate the active account, which will be used when targeting that host.
If an account on any host (or only the one given via `--hostname`) has authentication issues,
the command will exit with 1 and output to stderr.

To change the active account for a host, see `gh auth switch`.


USAGE
  gh auth status [flags]

FLAGS
  -a, --active            Display the active account only
  -h, --hostname string   Check only a specific hostname's auth status
      --json fields       Output JSON with the specified fields
  -t, --show-token        Display the auth token

INHERITED FLAGS
  --help   Show help for command

JSON FIELDS
  active, gitProtocol, login, token, scopes, state

LEARN MORE
  Use `gh <command> <subcommand> --help` for more information about a command.
  Read the manual at https://cli.github.com/manual
  Learn about exit codes using `gh help exit-codes`
  Learn about accessibility experiences using `gh help accessibility`
```
@babakks: Thanks for sharing your thought! @evilhamsterman @BagToad @andyfeller 

### Regarding the `--json` vs `--format` option

I'm okay to go with `--json <FIELDS>`, and also like the fact that this removes the need for the `--show-token` option. However, I think we should add a mutually exclusive restriction on using `--show-token` and `--json` option with an error message like:

```
`--json` and `--show-token` cannot be used together. To include the token in the JSON output, use `--json token`.
```

### Regarding the exit code

I couldn't even imagine the mentioned behaviour in `pr checks`, so thank you @BagToad for the hint. As I checked this behaviour is not even documented, which I think we should do that. That said, I'm torn between doing the keeping things consistent with `pr checks`, and the user-friendly choice (keep the exit code regime as is).

Given the number of votes on this issue, I can imagine there are probably a good number of users who have managed to parse the current `auth status` output. Once they discover the new `--json` option, they'd probably go for it (I would 😄) and that's my fear for them to see an unexpected behaviour. I know they are expected to read the docs, but still.

An argument in favour of keeping the current exit codes is that if we decide to exit with zero regardless of the token status, then a user who just wants to see if all tokens are valid, has to go over all entries and assert they're all `success`. They would also need to assert that stderr output is empty. An alternative way for the user is to just use `auth status` without the `--json` fields first and if it was a success then they'd go and call it with the `--json` option to grab information they needed. I don't know if it's a real scenario out there or not, but if it is it's a mild headache for the user.

> [!NOTE]
> Looking around, I noticed a rather interesting approach in `run view` and `run watch`. There's this `--exit-status` option that when provided enforces the command to exit with a non-zero exit code if the any of the jobs (or the single one provided via `--job` option) has failed. So, the default behaviour is to always exit with zero, unless `--exit-code` is there. We're not employing such an option in other places (at least to my knowledge and searches). I'd be nice if we had it for the `auth status` from the beginning, and we wouldn't have to decide things now.

Anyway, I think that's all I have in favour of keeping the exit codes. Let me know what you think @BagToad, and we can finalise the A/C.

### Regarding the field names

I agree with `login` instead of `user`.@BagToad: ## Exit codes

@babakks and I discussed this and we agreed that always exiting 0 with `--json` makes sense. This aligns with other commands with custom exit code functionality like `gh pr checks` where JSON output causes a 0 exit code. 

Exiting non-zero in exceptional cases where valid JSON cannot be returned (e.g. a panic), is still appropriate, of course. 

We definitely need the documentation of this exit code behavior to be added to the AC. 

## JSON option formats

@babakks and I discussed the pros and cons of the two options

1. `--format JSON`, requiring `--show-token` to unmask the token.
2. `--json <fields>`, NOT requiring `--show-token` to unmask the token (token is always unmasked in JSON output)

We decided that option 2 ☝️ makes the most sense, all things considered because the user must explicitly ask for the token. This aligns with decisions that have been made with `gh auth token` as well where we print the token in plain text because we trust that's what the user asked for. Pairing that with `--json <fields>` just being largely the more consistent option with other commands, makes this the best option, we think.
@babakks: 
Thanks, @BagToad, for the summary! 🙏

Now that we're going with the `--json <FIELDS>` option, the JSON entries should all have the same set of fields, regardless of their token status. So, here's the expected JSON structure (if all available fields were requested):

```jsonc
{
    "hostname": [
        {
            // valid token entry
            "state": "success",
            "error": "",                  // empty
            "active": true,               // or false
            "host": "hostname",
            "login": "username",
            "tokenSource": "oauth_token", // or an env var name
            "token": "...",
            "scopes": "...",
            "gitProtocol": "ssh",         // or "https"
        },
        {
            // invalid token entry
            "state": "error",
            "error": "[error message]",
            "active": false,              // or true
            "host": "hostname",
            "login": "username",
            "tokenSource": "oauth_token", // or an env var name
            "token": "...",
            "scopes": "",                 // empty
            "gitProtocol": ""             // empty
        },
        {
            // timeout entry
            "state": "timeout",
            "error": "[error message]",
            "active": false,              // or true
            "host": "hostname",
            "login": "username",
            "tokenSource": "oauth_token", // or an env var name
            "token": "...",
            "scopes": "",                 // empty
            "gitProtocol": ""             // empty
        },
    ],
    // ...
}
```

## Acceptance Criteria

> [!NOTE]
> - `<ALL-FIELDS>` is a placeholder for all fields.
> - `<ANY-FIELDS>` is a placeholder for any subset of the available fields.

### No tokens
**Given** I have no auth tokens on my host machine
**When** I run `gh auth status --json <ANY-FIELDS>`
**Then** I get an empty JSON on stdout (`{}`), and the message below on stderr, with a zero exit code.

```
You are not logged into any GitHub hosts. To log in, run: gh auth login
```

### No token for the given `--hostname`
**Given** I have no auth tokens for a GitHub host named `foo.com` on my host machine
**When** I run `gh auth status --hostname foo.com --json <ANY-FIELDS>`
**Then** I get an empty JSON on stdout (`{}`), and the message below on stderr, with a zero exit code.

```
You are not logged into any accounts on foo.com
```

### All valid tokens
**Given** I have valid auth tokens for one or more GitHub hosts (all valid tokens)
**When** I run `gh auth status --json <ALL-FIELDS>`
**Then** I get a JSON output like below with a zero exit code.

```jsonc
{
    "github.com": [
        {
            "state": "success",
            "error": "",                                // empty
            "active": true,
            "host": "github.com",
            "login": "my-username",
            "tokenSource": "...", 
            "token": "###############################", // (not a mask)
            "scopes": "...",
            "gitProtocol": "..."
        },
        // other accounts...
    ],
    // other hosts...
}
```

### All valid tokens, with `--hostname`
**Given** I have valid auth tokens for one or more GitHub hosts (all valid tokens)
**When** I run `gh auth status --hostname github.com --json state,active,host,login,tokenSource,scopes,gitProtocol,token`
**Then** I get a JSON output like below with a zero exit code. Note that there's only one hostname in the output.

```jsonc
{
    "github.com": [
        {
            "state": "success",
            "error": "",                                // empty
            "active": true,
            "host": "github.com",
            "login": "my-username",
            "tokenSource": "...", 
            "token": "###############################", // (not a mask)
            "scopes": "...",
            "gitProtocol": "...",
        },
        // other accounts...
    ],
}
```

### An invalid token
**Given** I have an invalid auth token (among other tokens)
**When** I run `gh auth status --json <ALL-FIELDS>`
**Then** I get a JSON output like below with zero exit code.

```jsonc
{
    "github.com": [
        {
            "state": "error",
            "error": "[error message]",
            "active": true,
            "host": "github.com",
            "login": "my-username",
            "tokenSource": "...", 
            "token": "###############################", // (not a mask)
            "scopes": "",                               // empty
            "gitProtocol": "",                          // empty
        },
        // other accounts...
    ],
    // other hosts...
}
```

### An unreachable host (timeout)
**Given** I have a valid (or invalid, doesn't matter) auth token that for an unreachable GitHub host
**When** I run `gh auth status --json <ALL-FIELDS>`
**Then** I get a JSON output like below with a zero exit code.

```jsonc
{
    "github.com": [
        {
            "state": "timeout",
            "error": "[error message]",
            "active": true,
            "host": "github.com",
            "login": "my-username",
            "tokenSource": "...",
            "token": "###############################", // (not a mask)
            "scopes": "",                               // empty
            "gitProtocol": "",                          // empty
        },
        // other accounts...
    ],
    // other hosts...
}
```

### With `--jq`
**Given** I have auth tokens on my host machine
**When** I run `gh auth status --json <ANY-FIELDS> --jq <QUERY>`
**Then** I get the result of applying the given `<QUERY>` on the JSON data.

### With `--template`
**Given** I have auth tokens on my host machine
**When** I run `gh auth status --json <ANY-FIELDS> --template <TEMPLATE>`
**Then** I get the result of rendering the given `<TEMPLATE>` from the JSON data.

### `--show-token` and `--json` usage
**When** I run `gh auth status --show-token --json <ANY-FIELDS>`
**Then** I get the error below with exit code 1.

```
`--json` and `--show-token` cannot be used together. To include the token in the JSON output, use `--json token`.
```@babakks: *This comment is for anyone who is interested to work on a PR for this issue.*

In the current implementation of the `auth status` command, there are different types for different token entries (e.g., `validEntry`, `timeoutErrorEntry`, or `invalidTokenEntry`). Although it seems like a fair design, now that we're going to introduce the JSON output it's much simpler to use a single type for entries. So, you might want to get rid of the existing types and consolidate them into a single type.

Also note that, fields like `tokenIsWriteable` can be dropped in favour using the `authTokenWriteable` function when it's needed.

With a single type for entries, you can simply use the below approach for making the type JSON exportable without any efforts (i.e. iterating over the fields, etc). 

https://github.com/cli/cli/blob/ca68f44ac7659a24f4bbc16ed9f160b56d4d6e3c/pkg/cmd/label/shared.go#L9-L33

You can also look at these examples on how to add the `--json`, `--jq`, and `--template` fields. Note that `cmdutil.AddJSONFlags` takes care of adding all three options.

https://github.com/cli/cli/blob/ca68f44ac7659a24f4bbc16ed9f160b56d4d6e3c/pkg/cmd/label/list.go#L79 

https://github.com/cli/cli/blob/ca68f44ac7659a24f4bbc16ed9f160b56d4d6e3c/pkg/cmd/label/list.go#L104-L107
@BagToad: @benjlevesque @evilhamsterman 👋 Since you folks expressed interest in taking a stab at this, it is ready for implementation following the acceptance criteria in https://github.com/cli/cli/issues/8637#issuecomment-3167167771. If anyone else is also interested, let us know, we can assign whoever to the issue 👍 @benjlevesque: Hey @BagToad thanks for the ping, I'd be happy to give this a try :) 

The AC seems pretty clear overall, but I have a few comments 

## Conflict in flag alias
 I noticed is that the `-t` short flag of `--show-token` conflicts with `--template` when using `AddJSONFlags`. What do you think should be done about this? Removing `-t` for `--show-token` would be a breaking change... Maybe make `-f` optional in  `AddJSONFlags`? I'll look into options...
 
## Error message for `--show-token` and `--json` usage
I've seen in other parts of the code that `MarkFlagsMutuallyExclusive` is used to mark flags exclusive. Would this be an acceptable solution, rather than a custom message?


@BagToad: > I noticed is that the `-t` short flag of `--show-token` conflicts with `--template` when using `AddJSONFlags`. What do you think should be done about this? Removing `-t` for `--show-token` would be a breaking change... Maybe make `-f` optional in `AddJSONFlags`? I'll look into options...

@benjlevesque that is a good question!

I think we should cut the short flag in this case 🤔 It sounds like you're on the right track with what you're looking into regarding `AddJSONFlags` 👍 Interested to see what it looks like in a PR and maybe we could spot other options.


> I've seen in other parts of the code that `MarkFlagsMutuallyExclusive` is used to mark flags exclusive. Would this be an acceptable solution, rather than a custom message?

I think that's reasonable. While I think it would be a slightly better user experience to have an explanation for why they're mutually exclusive like the AC suggests, if it makes the code unreasonable or requires way more work, then I don't think that specific error message should block the PR. Especially if we have the reason documented in the command's `Long` help doc output 🤔 

WDYT?@benjlevesque: @BagToad thanks for the response! I found a pretty good option for the `-t` conflict, let me know what you think in the [PR](https://github.com/cli/cli/pull/11544) :)

Regarding the exclusive flags, I do agree the message is clearer, I was only asking for consistency with other commands.

Please take a look at the PR and let me know what you think! 😊 

___

On another note, after testing the  `--json`, `--jq ` and `--template` options, I still find a flat array output (without `"github.com") would improve the UX.  Two main pain points:
 1. The --json fields don’t align with the top-level properties of the returned object.
 2. Using --template/--jq feels unnecessarily complex for this use case.

```bash
gh auth status --json login --template '{{ range (index . "github.com") }}{{ .login }}{{ end }}'
gh auth status --json login  --jq '.["github.com"].[].login'
```

With a flat array, it could be 
```bash
gh auth status --json login --hostname github.com --jq '.[].login' 
gh auth status --json login,host --jq '.[] | select(.host=="github.com") | .login'

gh auth status  --json login --hostname github.com  --template '{{ range . }}{{ .login }}{{ end }}' 
```

f you get a chance to test the flat array approach, I’d love to hear your thoughts about which approach feels more natural / easier to use 👍 @BagToad: Thank you @benjlevesque! 🙌 

I'm not sure about the flat array thing, but it sounds like it would be better 🤔 I would like @babakks's thoughts. 

----

> I found a pretty good option for the `-t` conflict, let me know what you think in the [PR](https://github.com/cli/cli/pull/11544) :)

Very cool idea! I am going to give it some thought to understand the impact of something like this, just in case 🤔 I'll comment more on the PR if I think of anything. @benjlevesque: @BagToad Regarding the `-t` flag, I didn’t verify if it would conflict with other flags, but I don’t think there’s a risk of collision since the code was crashing during execution when registering the second flag.

However, the current implementation has a downside: it could mask future conflicts by silently disabling the `-t` alias for `--template`. This might introduce breaking changes down the line, so it may not be the best approach.

Initially, I considered creating a separate function to register JSON flags without aliases and then manually adding the aliases afterward.

Let me know what you think!

@babakks: Thanks for picking up this work, @benjlevesque! 🍻 

Good call out regarding the `-t` conflict. I didn't notice it when I was writing the A/C. I Just commented (https://github.com/cli/cli/pull/11544#discussion_r2307131309) on your PR with a suggestion. Let's discuss there on the comment.

About the array/map structure, I have a few arguments in favour of maps:
- It's consistent with our human-friendly output which is grouped by hostnames.
- Looking up for a specific hostname is easier with a map.
- I kind of feel like most of the time automation scripts would prefer working with the JSON output and the `--jq` option over `--template`. It's not a 100% correct, of course, but to me the `--template` option is more like a way of reshaping the data for human readability.
- If a flat array is needed, (the seemingly redundant) `hostname` field is there in the entries which allows users to simply flatten the map without the need to somehow keep the map key. For example, jq's `add` function does what's needed:
  ```sh
  echo '{"foo":[{"bar":0},{"bar":1}],"baz":[{"bar":2}]}' | jq add
  # [
  #   {
  #     "bar": 0
  #   },
  #   {
  #     "bar": 1
  #   },
  #   {
  #     "bar": 2
  #   }
  # ]  
  ```

  Since some folks might prefer the flat array we can surely add an example in the `--help` docs to show them how to do it.

All said, I'm open to discussion if you strongly feel toward the flat array, @benjlevesque @BagToad @srid: FWIW: my main use case is to _programatically_ tell if the user has already `gh auth login`'ed once. @evilhamsterman: @srid it looks like @benjlevesque has a pr just about done so we should get this soon 🎆 @babakks: The PR for this is merged now and the JSON output will be available with the next release, which will be `v2.81.0`.

However, we ended up formatting the JSON output like this (note the top-level `hosts` field):

```jsonc
{
  "hosts": {
    "github.com": [/*...*/],
    "another.host": [/*...*/],
  }
}
```

This structure makes room for future extensibility without breaking automations (e.g. adding more top-level fields without them being interpreted as yet another host). This is more consistent with other `gh` commands. Also it'd be much easier for the user to invoke the command without the need to specify fields per entry, i.e.:

```
gh auth status --json hosts
```

To include the token in the JSON output, users can just add the `--show-token` (or `-t`):

```
gh auth status --json hosts --show-token
```@evilhamsterman: WOOHOO!  🥳 

```
$ gh auth status --json hosts
{
  "hosts": {
    "github.com": [
      {
        "state": "success",
        "active": true,
        "host": "github.com",
        "login": "evilhamsterman",
        "tokenSource": "keyring",
        "scopes": "gist, read:org, repo, workflow",
        "gitProtocol": "https"
      }
    ]
  }
}
```
Thanks so much @benjlevesque and @babakks! Now at some point when I get time I'll work on building a Docker credential plugin using `gh`

Solution patch:
diff --git a/pkg/cmd/auth/status/status.go b/pkg/cmd/auth/status/status.go
index 153c03c04..2139bda1e 100644
--- a/pkg/cmd/auth/status/status.go
+++ b/pkg/cmd/auth/status/status.go
@@ -145,8 +145,11 @@ func NewCmdStatus(f *cmdutil.Factory, runF func(*StatusOptions) error) *cobra.Co
 		Long: heredoc.Docf(`
 			Display active account and authentication state on each known GitHub host.
 
-			For each host, the authentication state of each known account is tested and any issues are included in
-			the output. Each host section will indicate the active account, which will be used when targeting that host.
+			For each host, the authentication state of each known account is tested and any issues are included in the output.
+			Each host section will indicate the active account, which will be used when targeting that host.
+			If an account on any host (or only the one given via %[1]s--hostname%[1]s) has authentication issues,
+			the command will exit with 1 and output to stderr.
+
 			To change the active account for a host, see %[1]sgh auth switch%[1]s.
 		`, "`"),
 		RunE: func(cmd *cobra.Command, args []string) error {
@@ -158,7 +161,7 @@ func NewCmdStatus(f *cmdutil.Factory, runF func(*StatusOptions) error) *cobra.Co
 		},
 	}
 
-	cmd.Flags().StringVarP(&opts.Hostname, "hostname", "h", "", "Check a specific hostname's auth status")
+	cmd.Flags().StringVarP(&opts.Hostname, "hostname", "h", "", "Check only a specific hostname's auth status")
 	cmd.Flags().BoolVarP(&opts.ShowToken, "show-token", "t", false, "Display the auth token")
 
 	return cmd
@@ -217,6 +220,10 @@ func statusRun(opts *StatusOptions) error {
 		})
 		statuses[hostname] = append(statuses[hostname], entry)
 
+		if err == nil && !isValidEntry(entry) {
+			err = cmdutil.SilentError
+		}
+
 		users := authCfg.UsersForHost(hostname)
 		for _, username := range users {
 			if username == activeUser {
@@ -233,6 +240,10 @@ func statusRun(opts *StatusOptions) error {
 				username:    username,
 			})
 			statuses[hostname] = append(statuses[hostname], entry)
+
+			if err == nil && !isValidEntry(entry) {
+				err = cmdutil.SilentError
+			}
 		}
 	}
 
@@ -243,15 +254,20 @@ func statusRun(opts *StatusOptions) error {
 			continue
 		}
 
+		stream := stdout
+		if err != nil {
+			stream = stderr
+		}
+
 		if prevEntry {
-			fmt.Fprint(stdout, "\n")
+			fmt.Fprint(stream, "\n")
 		}
 		prevEntry = true
-		fmt.Fprintf(stdout, "%s\n", cs.Bold(hostname))
-		fmt.Fprintf(stdout, "%s", strings.Join(entries.Strings(cs), "\n"))
+		fmt.Fprintf(stream, "%s\n", cs.Bold(hostname))
+		fmt.Fprintf(stream, "%s", strings.Join(entries.Strings(cs), "\n"))
 	}
 
-	return nil
+	return err
 }
 
 func displayToken(token string, printRaw bool) string {
@@ -356,3 +372,8 @@ func buildEntry(httpClient *http.Client, opts buildEntryOptions) Entry {
 func authTokenWriteable(src string) bool {
 	return !strings.HasSuffix(src, "_TOKEN")
 }
+
+func isValidEntry(entry Entry) bool {
+	_, ok := entry.(validEntry)
+	return ok
+}



Test patch:
diff --git a/pkg/cmd/auth/status/status_test.go b/pkg/cmd/auth/status/status_test.go
index ce4897978..03c26ca04 100644
--- a/pkg/cmd/auth/status/status_test.go
+++ b/pkg/cmd/auth/status/status_test.go
@@ -100,7 +100,8 @@ func Test_statusRun(t *testing.T) {
 					return nil, context.DeadlineExceeded
 				})
 			},
-			wantOut: heredoc.Doc(`
+			wantErr: cmdutil.SilentError,
+			wantErrOut: heredoc.Doc(`
 				github.com
 				  X Timeout trying to log in to github.com account monalisa (GH_CONFIG_DIR/hosts.yml)
 				  - Active account: true
@@ -159,7 +160,53 @@ func Test_statusRun(t *testing.T) {
 				// mock for HeaderHasMinimumScopes api requests to a non-github.com host
 				reg.Register(httpmock.REST("GET", "api/v3/"), httpmock.StatusStringResponse(400, "no bueno"))
 			},
+			wantErr: cmdutil.SilentError,
+			wantErrOut: heredoc.Doc(`
+				ghe.io
+				  X Failed to log in to ghe.io account monalisa-ghe (GH_CONFIG_DIR/hosts.yml)
+				  - Active account: true
+				  - The token in GH_CONFIG_DIR/hosts.yml is invalid.
+				  - To re-authenticate, run: gh auth login -h ghe.io
+				  - To forget about this account, run: gh auth logout -h ghe.io -u monalisa-ghe
+			`),
+		},
+		{
+			name: "bad token on other host",
+			opts: StatusOptions{
+				Hostname: "ghe.io",
+			},
+			cfgStubs: func(t *testing.T, c gh.Config) {
+				login(t, c, "github.com", "monalisa", "gho_abc123", "https")
+				login(t, c, "ghe.io", "monalisa-ghe", "gho_abc123", "https")
+			},
+			httpStubs: func(reg *httpmock.Registry) {
+				// mocks for HeaderHasMinimumScopes api requests to a non-github.com host
+				reg.Register(httpmock.REST("GET", "api/v3/"), httpmock.WithHeader(httpmock.ScopesResponder("repo,read:org"), "X-Oauth-Scopes", "repo, read:org"))
+			},
 			wantOut: heredoc.Doc(`
+				ghe.io
+				  ✓ Logged in to ghe.io account monalisa-ghe (GH_CONFIG_DIR/hosts.yml)
+				  - Active account: true
+				  - Git operations protocol: https
+				  - Token: gho_******
+				  - Token scopes: 'repo', 'read:org'
+			`),
+		},
+		{
+			name: "bad token on selected host",
+			opts: StatusOptions{
+				Hostname: "ghe.io",
+			},
+			cfgStubs: func(t *testing.T, c gh.Config) {
+				login(t, c, "github.com", "monalisa", "gho_abc123", "https")
+				login(t, c, "ghe.io", "monalisa-ghe", "gho_abc123", "https")
+			},
+			httpStubs: func(reg *httpmock.Registry) {
+				// mocks for HeaderHasMinimumScopes api requests to a non-github.com host
+				reg.Register(httpmock.REST("GET", "api/v3/"), httpmock.StatusStringResponse(400, "no bueno"))
+			},
+			wantErr: cmdutil.SilentError,
+			wantErrOut: heredoc.Doc(`
 				ghe.io
 				  X Failed to log in to ghe.io account monalisa-ghe (GH_CONFIG_DIR/hosts.yml)
 				  - Active account: true
@@ -355,7 +402,8 @@ func Test_statusRun(t *testing.T) {
 					httpmock.GraphQL(`query UserCurrent\b`),
 					httpmock.StringResponse(`{"data":{"viewer":{"login":"monalisa-ghe-2"}}}`))
 			},
-			wantOut: heredoc.Doc(`
+			wantErr: cmdutil.SilentError,
+			wantErrOut: heredoc.Doc(`
 				github.com
 				  ✓ Logged in to github.com account monalisa-2 (GH_CONFIG_DIR/hosts.yml)
 				  - Active account: true