Kuberwastaken/claurst · error · anyhow::Error

Invalid : contains unsafe characters

Error message

Invalid {}: contains unsafe characters

What it means

`run_mcp_auth_session` calls the `open` crate's `open::that(&session.auth_url)` to launch the user's default browser at the OAuth authorization URL. If spawning the browser fails, the OS error is wrapped in this anyhow message. The OAuth flow cannot proceed because no browser can display the consent page.

Solutions

  1. Run the auth flow in a desktop session where a default browser is configured, or set the `BROWSER` env var to a usable browser binary.
  2. Manually copy the auth URL from logs/output into a browser on any machine, then complete the flow (the local listener still waits for the callback).
  3. Check the wrapped OS error message to identify the missing browser/handler.
  4. On servers, use an SSH tunnel for the callback port so a browser on another host can reach the redirect URI.

Example fix

// before: fails headless because open::that cannot spawn a browser
// after: fall back to printing the URL for manual opening
if let Err(e) = open::that(&session.auth_url) {
    eprintln!("Failed to open browser for OAuth: {}", e);
    eprintln!("Open this URL manually: {}", session.auth_url);
}
Defensive patterns

Strategy: fallback

Try / catch

if let Err(e) = open::that(&session.auth_url) {
    eprintln!("Could not open browser: {}. Open manually:\n{}", e, session.auth_url);
}

Prevention

When it happens

Trigger: `open::that()` returns Err — no default browser configured, the browser executable is missing, the process cannot be spawned (headless server, SSH session without display), or the OS denies launching the handler.

Common situations: Running the auth flow over SSH or in a headless CI/container with no `$BROWSER`/desktop environment; macOS/Linux default-browser association broken; restricted sandbox blocking process spawn.

Related errors


AI-assisted analysis of Kuberwastaken/claurst@b0637c97ec (2026-09-10). Data as JSON: /api/errors/bfc85af0faa6288c. Report an issue: GitHub.

Appendix: source

Thrown at src-rust/crates/bridge/src/lib.rs:231

        }

        config
    }

    /// Returns `true` only when the bridge is both enabled and has a token.
    pub fn is_active(&self) -> bool {
        self.enabled && self.session_token.is_some()
    }

    /// Validate that a server-provided ID is safe to interpolate into a URL
    /// path segment. Prevents path traversal (e.g. `../../admin`).
    ///
    /// Mirrors `validateBridgeId()` in `bridgeApi.ts`.
    pub fn validate_id<'a>(id: &'a str, label: &str) -> anyhow::Result<&'a str> {
        static RE: std::sync::OnceLock<regex::Regex> = std::sync::OnceLock::new();
        let re = RE.get_or_init(|| regex::Regex::new(r"^[a-zA-Z0-9_-]+$").unwrap());
        if id.is_empty() || !re.is_match(id) {
            anyhow::bail!("Invalid {}: contains unsafe characters", label);
        }
        Ok(id)
    }
}

// ---------------------------------------------------------------------------
// Permission decision
// ---------------------------------------------------------------------------

/// A tool-use permission decision sent by the web UI back to the CLI.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum PermissionDecision {
    Allow,
    AllowPermanently,
    Deny,
    DenyPermanently,
}

View on GitHub (pinned to b0637c97ec)