atuinsh/atuin · error

{}

Error message

{}

What it means

In `HubAuthClient::login`, a 403 (FORBIDDEN) response whose body parses as `HubErrorResponse` is turned into this bail carrying the server's `reason` string, unless the code is `2fa_required` (which returns TwoFactorRequired instead). The message text is dynamic — it is whatever the Hub server put in `reason` — so it can represent many refusal causes.

Solutions

  1. Read the `reason` text in the error — it comes verbatim from the Hub and states the specific cause
  2. Check account status on the Atuin Hub web UI (suspension, verification required)
  3. Complete email verification or contact Hub support if the account is flagged
  4. If 2FA is involved, supply a valid `totp_code` so the server doesn't reach a forbidden state
  5. Update the Atuin CLI if the server indicates the client version is rejected

Example fix

null
Defensive patterns

Strategy: try-catch

Type guard

fn is_hub_forbidden_reason(err: &Report) -> Option<String> {
    let msg = err.to_string();
    if msg == "invalid credentials" || msg.starts_with("Hub login failed") { None }
    else { Some(msg) } // 403 reason text is surfaced verbatim
}

Try / catch

match hub.login(username, password, totp).await {
    Ok(AuthResponse::Success { .. }) => println!("logged in"),
    Ok(AuthResponse::TwoFactorRequired) => {
        // prompt for TOTP and retry with the code
    }
    Err(e) => {
        // 403 reason is server-provided: display it and check account status on the Hub
        eprintln!("login refused by Hub: {e}");
    }
}

Prevention

When it happens

Trigger: POSTing to `api/v0/login` and receiving 403 with a JSON body `{"reason": ..., "code": ...}` where `code != "2fa_required"` — e.g. account disabled/suspended, login forbidden from this client, or any server-side policy rejection.

Common situations: Account suspended or banned on Atuin Hub; organization/IP policy blocking login; server-side maintenance flags; using a client version the server no longer permits (server rejects with explanatory reason).

Related errors


AI-assisted analysis of atuinsh/atuin@900007c480 (2026-09-19). Data as JSON: /api/errors/19c84ee9e22f8194. Report an issue: GitHub.

Appendix: source

Thrown at crates/atuin-client/src/auth.rs:312

            .context("failed to connect to Atuin Hub")?;

        let status = resp.status();

        if status.is_success() {
            let login: LoginResponse = resp.json().await?;
            return Ok(AuthResponse::Success {
                session: login.session,
                auth_type: login.auth,
            });
        }

        if status == StatusCode::FORBIDDEN
            && let Ok(err) = resp.json::<HubErrorResponse>().await
        {
            if err.code.as_deref() == Some("2fa_required") {
                return Ok(AuthResponse::TwoFactorRequired);
            }
            bail!("{}", err.reason);
        }

        if status == StatusCode::UNAUTHORIZED {
            bail!("invalid credentials");
        }

        bail!("Hub login failed with status {status}");
    }

    async fn register(&self, username: &str, email: &str, password: &str) -> Result<AuthResponse> {
        let url = self.address.append_path("api/v0/register")?;
        let client = reqwest::Client::new();

        let resp = client
            .post(url)
            .header(USER_AGENT, APP_USER_AGENT)
            .header(ATUIN_HEADER_VERSION, ATUIN_CARGO_VERSION)
            .json(&serde_json::json!({

View on GitHub (pinned to 900007c480)