diff --git a/Cargo.lock b/Cargo.lock
index 4ea41ee..cc8b9fd 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -122,6 +122,9 @@ name = "cc"
 version = "1.0.79"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f"
+dependencies = [
+ "jobserver",
+]
 
 [[package]]
 name = "cfg-if"
@@ -271,6 +274,7 @@ dependencies = [
  "eyre",
  "forgejo-api",
  "futures",
+ "git2",
  "open",
  "serde",
  "serde_json",
@@ -428,6 +432,21 @@ version = "0.27.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b6c80984affa11d98d1b88b66ac8853f143217b399d3c74116778ff8fdb4ed2e"
 
+[[package]]
+name = "git2"
+version = "0.17.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7b989d6a7ca95a362cf2cfc5ad688b3a467be1f87e480b8dad07fee8c79b0044"
+dependencies = [
+ "bitflags 1.3.2",
+ "libc",
+ "libgit2-sys",
+ "log",
+ "openssl-probe",
+ "openssl-sys",
+ "url",
+]
+
 [[package]]
 name = "h2"
 version = "0.3.20"
@@ -624,6 +643,15 @@ version = "1.0.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "62b02a5381cc465bd3041d84623d0fa3b66738b52b8e2fc3bab8ad63ab032f4a"
 
+[[package]]
+name = "jobserver"
+version = "0.1.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "936cfd212a0155903bcbc060e316fb6cc7cbf2e1907329391ebadc1fe0ce77c2"
+dependencies = [
+ "libc",
+]
+
 [[package]]
 name = "js-sys"
 version = "0.3.64"
@@ -645,6 +673,46 @@ version = "0.2.147"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3"
 
+[[package]]
+name = "libgit2-sys"
+version = "0.15.2+1.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a80df2e11fb4a61f4ba2ab42dbe7f74468da143f1a75c74e11dee7c813f694fa"
+dependencies = [
+ "cc",
+ "libc",
+ "libssh2-sys",
+ "libz-sys",
+ "openssl-sys",
+ "pkg-config",
+]
+
+[[package]]
+name = "libssh2-sys"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2dc8a030b787e2119a731f1951d6a773e2280c660f8ec4b0f5e1505a386e71ee"
+dependencies = [
+ "cc",
+ "libc",
+ "libz-sys",
+ "openssl-sys",
+ "pkg-config",
+ "vcpkg",
+]
+
+[[package]]
+name = "libz-sys"
+version = "1.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56ee889ecc9568871456d42f603d6a0ce59ff328d291063a45cbdf0036baf6db"
+dependencies = [
+ "cc",
+ "libc",
+ "pkg-config",
+ "vcpkg",
+]
+
 [[package]]
 name = "linux-raw-sys"
 version = "0.3.8"
diff --git a/Cargo.toml b/Cargo.toml
index c430c0c..291646c 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -11,6 +11,7 @@ directories = "5.0.1"
 eyre = "0.6.8"
 forgejo-api = { path = "./forgejo-api" }
 futures = "0.3.28"
+git2 = "0.17.2"
 open = "5.0.0"
 serde = { version = "1.0.170", features = ["derive"] }
 serde_json = "1.0.100"
diff --git a/forgejo-api/src/lib.rs b/forgejo-api/src/lib.rs
index 0eb1940..d5bd6a0 100644
--- a/forgejo-api/src/lib.rs
+++ b/forgejo-api/src/lib.rs
@@ -1,7 +1,7 @@
+use reqwest::{Client, Request, StatusCode};
 use serde::{de::DeserializeOwned, Serialize};
-use url::Url;
 use soft_assert::*;
-use reqwest::{Client, StatusCode, Request};
+use url::Url;
 
 pub struct Forgejo {
     url: Url,
@@ -23,7 +23,7 @@ pub enum ForgejoError {
     #[error("unexpected status code {} {}", .0.as_u16(), .0.canonical_reason().unwrap_or(""))]
     UnexpectedStatusCode(StatusCode),
     #[error("{} {}: {}", .0.as_u16(), .0.canonical_reason().unwrap_or(""), .1)]
-    ApiError(StatusCode, String)
+    ApiError(StatusCode, String),
 }
 
 impl From<reqwest::Error> for ForgejoError {
@@ -37,23 +37,32 @@ impl From<reqwest::Error> for ForgejoError {
 }
 
 impl Forgejo {
-    pub fn new(api_key: &str, url: Url) -> Result<Self, ForgejoError> { 
+    pub fn new(api_key: &str, url: Url) -> Result<Self, ForgejoError> {
         Self::with_user_agent(api_key, url, "forgejo-api-rs")
     }
 
-    pub fn with_user_agent(api_key: &str, url: Url, user_agent: &str) -> Result<Self, ForgejoError> {
-        soft_assert!(matches!(url.scheme(), "http" | "https"), Err(ForgejoError::HttpRequired));
+    pub fn with_user_agent(
+        api_key: &str,
+        url: Url,
+        user_agent: &str,
+    ) -> Result<Self, ForgejoError> {
+        soft_assert!(
+            matches!(url.scheme(), "http" | "https"),
+            Err(ForgejoError::HttpRequired)
+        );
 
         let mut headers = reqwest::header::HeaderMap::new();
-        let mut key_header: reqwest::header::HeaderValue = format!("token {api_key}").try_into().map_err(|_| ForgejoError::KeyNotAscii)?;
+        let mut key_header: reqwest::header::HeaderValue = format!("token {api_key}")
+            .try_into()
+            .map_err(|_| ForgejoError::KeyNotAscii)?;
         // key_header.set_sensitive(true);
         headers.insert("Authorization", key_header);
-        let client = Client::builder().user_agent(user_agent).default_headers(headers).build()?;
+        let client = Client::builder()
+            .user_agent(user_agent)
+            .default_headers(headers)
+            .build()?;
         dbg!(&client);
-        Ok(Self { 
-            url,
-            client,
-        })
+        Ok(Self { url, client })
     }
 
     pub async fn get_repo(&self, user: &str, repo: &str) -> Result<Option<Repo>, ForgejoError> {
@@ -93,29 +102,42 @@ impl Forgejo {
         self.execute_opt(request).await
     }
 
-    async fn post<T: Serialize, U: DeserializeOwned>(&self, path: &str, body: &T) -> Result<U, ForgejoError> {
+    async fn post<T: Serialize, U: DeserializeOwned>(
+        &self,
+        path: &str,
+        body: &T,
+    ) -> Result<U, ForgejoError> {
         let url = self.url.join("api/v1/").unwrap().join(path).unwrap();
         let request = self.client.post(url).json(body).build()?;
         self.execute(request).await
-    } 
+    }
 
     async fn execute<T: DeserializeOwned>(&self, request: Request) -> Result<T, ForgejoError> {
         let response = self.client.execute(dbg!(request)).await?;
         match response.status() {
             status if status.is_success() => Ok(response.json::<T>().await?),
-            status if status.is_client_error() => Err(ForgejoError::ApiError(status, response.json::<ErrorMessage>().await?.message)),
-            status => Err(ForgejoError::UnexpectedStatusCode(status))
+            status if status.is_client_error() => Err(ForgejoError::ApiError(
+                status,
+                response.json::<ErrorMessage>().await?.message,
+            )),
+            status => Err(ForgejoError::UnexpectedStatusCode(status)),
         }
     }
 
     /// Like `execute`, but returns `Ok(None)` on 404.
-    async fn execute_opt<T: DeserializeOwned>(&self, request: Request) -> Result<Option<T>, ForgejoError> {
+    async fn execute_opt<T: DeserializeOwned>(
+        &self,
+        request: Request,
+    ) -> Result<Option<T>, ForgejoError> {
         let response = self.client.execute(dbg!(request)).await?;
         match response.status() {
             status if status.is_success() => Ok(Some(response.json::<T>().await?)),
             StatusCode::NOT_FOUND => Ok(None),
-            status if status.is_client_error() => Err(ForgejoError::ApiError(status, response.json::<ErrorMessage>().await?.message)),
-            status => Err(ForgejoError::UnexpectedStatusCode(status))
+            status if status.is_client_error() => Err(ForgejoError::ApiError(
+                status,
+                response.json::<ErrorMessage>().await?.message,
+            )),
+            status => Err(ForgejoError::UnexpectedStatusCode(status)),
         }
     }
 }
@@ -124,14 +146,13 @@ impl Forgejo {
 struct ErrorMessage {
     message: String,
     // intentionally ignored, no need for now
-    // url: Url 
+    // url: Url
 }
 
-
 #[derive(serde::Deserialize, Debug, PartialEq)]
 pub struct Repo {
     pub clone_url: Url,
-    #[serde(with="time::serde::rfc3339")]
+    #[serde(with = "time::serde::rfc3339")]
     pub created_at: time::OffsetDateTime,
     pub default_branch: String,
     pub description: String,
@@ -146,7 +167,7 @@ pub struct Repo {
 pub struct User {
     pub active: bool,
     pub avatar_url: Url,
-    #[serde(with="time::serde::rfc3339")]
+    #[serde(with = "time::serde::rfc3339")]
     pub created: time::OffsetDateTime,
     pub description: String,
     pub email: String,
@@ -156,7 +177,7 @@ pub struct User {
     pub id: u64,
     pub is_admin: bool,
     pub language: String,
-    #[serde(with="time::serde::rfc3339")]
+    #[serde(with = "time::serde::rfc3339")]
     pub last_login: time::OffsetDateTime,
     pub location: String,
     pub login: String,
@@ -189,7 +210,7 @@ pub struct CreateRepoOption {
     pub private: bool,
     pub readme: String,
     pub template: bool,
-    pub trust_model: TrustModel
+    pub trust_model: TrustModel,
 }
 
 #[derive(serde::Serialize, Debug, PartialEq)]
@@ -199,4 +220,4 @@ pub enum TrustModel {
     Committer,
     #[serde(rename = "collaboratorcommiter")]
     CollaboratorCommitter,
-}
\ No newline at end of file
+}
diff --git a/src/main.rs b/src/main.rs
index d6ff42c..24dca9c 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -26,8 +26,8 @@ pub enum Command {
 
 #[derive(Subcommand, Clone, Debug)]
 pub enum RepoCommand {
-    Create { 
-        host: String, 
+    Create {
+        host: String,
         repo: String,
 
         // flags
@@ -41,7 +41,7 @@ pub enum RepoCommand {
         /// Pushes the current branch to the default branch on the new repo.
         /// Implies `--set-upstream=origin` (setting upstream manual overrides this)
         #[clap(long, short)]
-        push: bool
+        push: bool,
     },
     Info,
     Browse,
@@ -81,9 +81,9 @@ async fn main() -> eyre::Result<()> {
 
     match args.command {
         Command::Repo(repo_subcommand) => match repo_subcommand {
-            RepoCommand::Create { 
-                host, 
-                repo ,
+            RepoCommand::Create {
+                host,
+                repo,
 
                 description,
                 private,
@@ -91,10 +91,12 @@ async fn main() -> eyre::Result<()> {
                 push,
             } => {
                 // let (host_domain, host_keys, repo) = keys.get_current_host_and_repo().await?;
-                let host_info = keys.hosts.get(&host).ok_or_else(|| eyre!("not a known host"))?;
+                let host_info = keys
+                    .hosts
+                    .get(&host)
+                    .ok_or_else(|| eyre!("not a known host"))?;
                 let (_, user) = host_info.get_current_user()?;
-                let url = Url::parse(&format!("http://{host}/"))?;
-                let api = Forgejo::new(&user.key, url.clone())?;
+                let api = Forgejo::new(&user.key, host_info.url.clone())?;
                 let repo_spec = CreateRepoOption {
                     auto_init: false,
                     default_branch: "main".into(),
@@ -109,41 +111,28 @@ async fn main() -> eyre::Result<()> {
                     trust_model: forgejo_api::TrustModel::Default,
                 };
                 let new_repo = api.create_repo(repo_spec).await?;
-                eprintln!("created new repo at {}", url.join(&format!("{}/{}", user.name, repo))?);
+                eprintln!(
+                    "created new repo at {}",
+                    host_info.url.join(&format!("{}/{}", user.name, repo))?
+                );
 
                 let upstream = set_upstream.as_deref().unwrap_or("origin");
 
-                if set_upstream.is_some() || push {
-                    let status = tokio::process::Command::new("git")
-                        .arg("remote")
-                        .arg("add")
-                        .arg(upstream)
-                        .arg(new_repo.clone_url.as_str())
-                        .status()
-                        .await?;
-                    if !status.success() {
-                        eprintln!("origin set failed");
-                    }
-                }
+                let repo = git2::Repository::open(".")?;
+                let remote = if set_upstream.is_some() || push {
+                    repo.remote(upstream, new_repo.clone_url.as_str())?;
+                } else {
+                    repo.find_remote(upstream)?;
+                };
 
                 if push {
-                    let status = tokio::process::Command::new("git")
-                        .arg("push")
-                        .arg("-u")
-                        .arg(upstream)
-                        .arg("main")
-                        .status()
-                        .await?;
-                    if !status.success() {
-                        eprintln!("push failed");
-                    }
+                    remote.push(upstream)?;
                 }
             }
             RepoCommand::Info => {
-                let (host_domain, host_keys, repo) = keys.get_current_host_and_repo().await?;
+                let (_, host_keys, repo) = keys.get_current_host_and_repo().await?;
                 let (_, user) = host_keys.get_current_user()?;
-                let url = Url::parse(&format!("http://{host_domain}/"))?;
-                let api = Forgejo::new(&user.key, url)?;
+                let api = Forgejo::new(&user.key, host_keys.url.clone())?;
                 let repo = api.get_repo(&user.name, &repo).await?;
                 match repo {
                     Some(repo) => {
@@ -153,19 +142,27 @@ async fn main() -> eyre::Result<()> {
                 }
             }
             RepoCommand::Browse => {
-                let (host_domain, host_keys, repo) = keys.get_current_host_and_repo().await?;
+                let (_, host_keys, repo) = keys.get_current_host_and_repo().await?;
                 let (_, user) = host_keys.get_current_user()?;
-                open::that(format!("http://{host_domain}/{}/{repo}", user.name))?;
+                open::that(
+                    host_keys
+                        .url
+                        .join(&format!("/{}/{repo}", user.name))?
+                        .as_str(),
+                )?;
             }
         },
         Command::User { host } => {
-            let (host_domain, host_keys) = match host.as_deref() {
-                Some(s) => (s, keys.hosts.get(s).ok_or_else(|| eyre!("not a known host"))?),
+            let (_, host_keys) = match host.as_deref() {
+                Some(s) => (
+                    s,
+                    keys.hosts.get(s).ok_or_else(|| eyre!("not a known host"))?,
+                ),
                 None => keys.get_current_host().await?,
             };
             let (_, info) = host_keys.get_current_user()?;
-            eprintln!("currently signed in to {}@{}", info.name, host_domain);
-        },
+            eprintln!("currently signed in to {}@{}", info.name, host_keys.url);
+        }
         Command::Auth(auth_subcommand) => match auth_subcommand {
             AuthCommand::Login => {
                 todo!();
@@ -206,7 +203,10 @@ async fn main() -> eyre::Result<()> {
                 name,
                 key,
             } => {
-                let host_keys = keys.hosts.entry(host.clone()).or_default();
+                let host_keys = keys
+                    .hosts
+                    .get_mut(&host)
+                    .ok_or_else(|| eyre!("unknown host {host}"))?;
                 let key = match key {
                     Some(key) => key,
                     None => readline("new key: ").await?,
@@ -254,30 +254,16 @@ async fn readline(msg: &str) -> eyre::Result<String> {
 }
 
 async fn get_remotes() -> eyre::Result<Vec<(String, Url)>> {
-    let remotes = String::from_utf8(
-        tokio::process::Command::new("git")
-            .arg("remote")
-            .output()
-            .await?
-            .stdout,
-    )?;
-    let remotes = futures::future::try_join_all(remotes.lines().map(|name| async {
-        let name = name.trim();
-        let url = Url::parse(
-            String::from_utf8(
-                tokio::process::Command::new("git")
-                    .arg("remote")
-                    .arg("get-url")
-                    .arg(name)
-                    .output()
-                    .await?
-                    .stdout,
-            )?
-            .trim(),
-        )?;
-        Ok::<_, eyre::Report>((name.to_string(), url))
-    }))
-    .await?;
+    let repo = git2::Repository::open(".")?;
+    let remotes = repo
+        .remotes()?
+        .iter()
+        .filter_map(|name| {
+            let name = name?.to_string();
+            let url = Url::parse(repo.find_remote(&name).ok()?.url()?).ok()?;
+            Some((name, url))
+        })
+        .collect::<Vec<_>>();
     Ok(remotes)
 }
 
@@ -295,9 +281,14 @@ async fn get_remote(remotes: &[(String, Url)]) -> eyre::Result<Url> {
 #[derive(serde::Serialize, serde::Deserialize, Clone, Default)]
 struct KeyInfo {
     hosts: BTreeMap<String, HostInfo>,
+    domain_to_name: BTreeMap<String, String>,
 }
 
 impl KeyInfo {
+    fn domain_to_name(&self, domain: &str) -> Option<&str> {
+        self.domain_to_name.get(domain).map(|s| &**s)
+    }
+
     async fn load() -> eyre::Result<Self> {
         let path = directories::ProjectDirs::from("", "Cyborus", "forgejo-cli")
             .ok_or_else(|| eyre!("Could not find data directory"))?
@@ -342,10 +333,13 @@ impl KeyInfo {
         } else {
             host_str.to_owned()
         };
+        let name = self
+            .domain_to_name(&domain)
+            .ok_or_else(|| eyre!("unknown remote"))?;
 
         let (name, host) = self
             .hosts
-            .get_key_value(&domain)
+            .get_key_value(name)
             .ok_or_else(|| eyre!("not signed in to {domain}"))?;
         Ok((name, host, repo_from_url(&remote)?.into()))
     }
@@ -381,9 +375,10 @@ fn repo_from_url(url: &Url) -> eyre::Result<&str> {
     Ok(repo)
 }
 
-#[derive(serde::Serialize, serde::Deserialize, Clone, Default)]
+#[derive(serde::Serialize, serde::Deserialize, Clone)]
 struct HostInfo {
     default: Option<String>,
+    url: Url,
     users: BTreeMap<String, UserInfo>,
 }
 
@@ -393,10 +388,7 @@ impl HostInfo {
             let (s, k) = self.users.first_key_value().unwrap();
             return Ok((s, k));
         }
-        if let Some(default) = self
-            .default
-            .as_ref()
-        {
+        if let Some(default) = self.default.as_ref() {
             if let Some(default_info) = self.users.get(default) {
                 return Ok((default, default_info));
             }