## TL;DR

`netlify login` never completes in a headless shell, SSH session, CI runner, or agent sandbox because it opens a browser for OAuth and waits forever for an authorization that can never arrive. Skip the interactive login: create a personal access token in the Netlify dashboard (User settings, Applications, Personal access tokens), then export it as NETLIFY_AUTH_TOKEN. The CLI picks the token up automatically and never prompts.

```bash
export NETLIFY_AUTH_TOKEN [your value]
export NETLIFY_SITE_ID=YOUR_SITE_API_ID
netlify status
```

If `netlify status` shows your account instead of prompting for login, you are authenticated.

## Symptom

```text
netlify login
```

The command prints a message about opening an authorization page and then sits there indefinitely. No error is returned; it just never finishes. This is the expected behavior when there is no browser to complete the OAuth loop.

## Fix it

### Step 1: stop the hanging command

Press Ctrl+C. Nothing was authenticated, so there is nothing to clean up.

### Step 2: create a personal access token in the dashboard

1. In a browser, open the Netlify dashboard.
2. Go to User settings, then Applications, then Personal access tokens.
3. Choose New access token, give it a name that says where it will be used (for example "CI deploy" or "agent sandbox"), and generate it.
4. Copy the token now. It is shown once.

Expected: a token string you can paste into the shell. Keep it out of repos and chat logs; it is a secret.

### Step 3: export the token in the non-interactive shell

```bash
export NETLIFY_AUTH_TOKEN [your value]
```

The CLI reads NETLIFY_AUTH_TOKEN automatically for every command. Alternatively, most commands accept `--auth YOUR_TOKEN` directly.

Expected: subsequent commands do not prompt for login.

### Step 4: set the site ID for deploys

```bash
export NETLIFY_SITE_ID=YOUR_SITE_API_ID
```

NETLIFY_AUTH_TOKEN only authenticates you; it does not pick a site. Locally, `netlify link` writes the site ID into `.netlify/state.json`, but a fresh CI or sandbox shell has no such file, so set NETLIFY_SITE_ID to the site API ID (shown as Project ID in the site configuration). Find the ID with `netlify sites:list` once the token is set.

### Step 5: verify

```bash
netlify status
```

Expected: output shows your Netlify account and any linked site, instead of launching a login prompt.

## When this applies

- `netlify login` hangs with no further output in SSH sessions, Docker containers, CI runners, or agent sandboxes.
- Any "waiting for authorization" style stall where no browser window can open.
- Scripted or unattended setups that must authenticate the Netlify CLI once and run hands-off.

## When it does NOT apply

- A local machine with a working browser: `netlify login` is the intended flow there; the token path also works but is unnecessary.
- Accounts behind Netlify SSO that require interactive SSO login: the personal access token may not satisfy the SSO requirement, and the org admin decides the allowed flow.
- Auth errors (exit code 2) after the token is set: that is a wrong, expired, or revoked token, not the hang. Generate a fresh token.
- Deploy or build failures that happen after authentication succeeds: those are site configuration problems, not login problems.

## Version compatibility

- Applies to all recent netlify-cli versions (requires Node.js 18.14.0+). The NETLIFY_AUTH_TOKEN and NETLIFY_SITE_ID variables have been the documented CI path for years.
- CLI exit codes: 0 success, 1 general error, 2 auth error, 3 project not found, 4 build failed.

## Variant phrasings

- "netlify login hangs" / "netlify login stuck waiting for authorization"
- "netlify login no browser" / "netlify login in SSH hangs"
- "netlify CLI headless login" / "authenticate netlify-cli in CI"

## Why it happens (root cause)

`netlify login` implements browser-based OAuth: it opens the authorization page in your default browser and polls for the callback that completes the login. In an environment with no browser, no display, and no way to open a URL interactively, that callback can never happen, so the CLI waits indefinitely. It is not an error or a bug; the command is simply waiting on input that cannot arrive. The personal access token path exists precisely to bypass the interactive flow for machines.

## Edge cases

- **Token storage:** put NETLIFY_AUTH_TOKEN in the CI provider secret store or the shell profile of the machine, never in the repo, in `netlify.toml`, or in build logs.
- **Token rotation:** tokens can be revoked from the same Personal access tokens page. A sudden auth failure on a previously working setup usually means the token was revoked or regenerated.
- **Multiple sites:** one token works for every site on the account; NETLIFY_SITE_ID decides which site each shell targets. Unset it to switch sites.
- **`netlify logout`:** clears the locally stored interactive token in the CLI config, but does not touch NETLIFY_AUTH_TOKEN. If logout seems to do nothing, the env var is still set.
- **Anonymous deploys:** for quick throwaway tests with no sensitive content, the CLI can do an anonymous deploy that expires within an hour; useful to check whether the problem is auth at all.