diff --git a/iroh-api/Cargo.toml b/iroh-api/Cargo.toml
index 361607917ed..391d3779891 100644
--- a/iroh-api/Cargo.toml
+++ b/iroh-api/Cargo.toml
@@ -27,4 +27,7 @@ futures = "0.3.21"
 async-stream = "0.3.3"
 mockall = { version = "0.11.2", optional = true }
 serde = { version = "1.0", features = ["derive"] }
+relative-path = "1.7.2"
 
+[dev-dependencies]
+tempdir = "0.3.7"
\ No newline at end of file
diff --git a/iroh-api/src/api.rs b/iroh-api/src/api.rs
index 64fab98df4b..ceee5b68f5a 100644
--- a/iroh-api/src/api.rs
+++ b/iroh-api/src/api.rs
@@ -5,39 +5,55 @@ use crate::config::{Config, CONFIG_FILE_NAME, ENV_PREFIX};
 #[cfg(feature = "testing")]
 use crate::p2p::MockP2p;
 use crate::p2p::{ClientP2p, P2p};
+use crate::{Cid, IpfsPath};
 use anyhow::Result;
-use async_trait::async_trait;
-use cid::Cid;
+use futures::future::{BoxFuture, LocalBoxFuture};
 use futures::stream::LocalBoxStream;
+use futures::FutureExt;
 use futures::StreamExt;
-use iroh_resolver::resolver::Path as IpfsPath;
-use iroh_resolver::{resolver, unixfs_builder};
+use iroh_resolver::unixfs_builder;
 use iroh_rpc_client::Client;
 use iroh_rpc_client::StatusTable;
 use iroh_util::{iroh_config_path, make_config};
 #[cfg(feature = "testing")]
 use mockall::automock;
+use relative_path::RelativePathBuf;
+use tokio::io::AsyncRead;
 
 pub struct Iroh {
     client: Client,
 }
 
-pub enum OutType<T: resolver::ContentLoader> {
+pub enum OutType {
     Dir,
-    Reader(resolver::OutPrettyReader<T>),
+    Reader(Box<dyn AsyncRead + Unpin>),
 }
 
+// Note: `#[async_trait]` is deliberately not in use for this trait, because it
+// became very hard to express what we wanted once streams were involved.
+// Instead we spell things out explicitly without magic.
+
 #[cfg_attr(feature= "testing", automock(type P = MockP2p;))]
-#[async_trait(?Send)]
 pub trait Api {
     type P: P2p;
 
     fn p2p(&self) -> Result<Self::P>;
 
-    async fn get<'a>(&self, ipfs_path: &IpfsPath, output: Option<&'a Path>) -> Result<()>;
-    async fn add(&self, path: &Path, recursive: bool, no_wrap: bool) -> Result<Cid>;
-    async fn check(&self) -> StatusTable;
-    async fn watch<'a>(&self) -> LocalBoxStream<'a, StatusTable>;
+    /// Produces a asynchronous stream of file descriptions
+    /// Each description is a tuple of a relative path, and either a `Directory` or a `Reader`
+    /// with the file contents.
+    fn get_stream(
+        &self,
+        ipfs_path: &IpfsPath,
+    ) -> LocalBoxStream<'_, Result<(RelativePathBuf, OutType)>>;
+    fn add<'a>(
+        &'a self,
+        path: &'a Path,
+        recursive: bool,
+        no_wrap: bool,
+    ) -> LocalBoxFuture<'_, Result<Cid>>;
+    fn check(&self) -> BoxFuture<'_, StatusTable>;
+    fn watch(&self) -> LocalBoxFuture<'static, LocalBoxStream<'static, StatusTable>>;
 }
 
 impl Iroh {
@@ -67,13 +83,8 @@ impl Iroh {
     fn from_client(client: Client) -> Self {
         Self { client }
     }
-
-    pub(crate) fn get_client(&self) -> &Client {
-        &self.client
-    }
 }
 
-#[async_trait(?Send)]
 impl Api for Iroh {
     type P = ClientP2p;
 
@@ -82,45 +93,67 @@ impl Api for Iroh {
         Ok(ClientP2p::new(p2p_client.clone()))
     }
 
-    async fn get<'b>(&self, ipfs_path: &IpfsPath, output: Option<&'b Path>) -> Result<()> {
-        let blocks = self.get_stream(ipfs_path, output);
-        tokio::pin!(blocks);
-        while let Some(block) = blocks.next().await {
-            let (path, out) = block?;
-            match out {
-                OutType::Dir => {
-                    tokio::fs::create_dir_all(path).await?;
+    fn get_stream(
+        &self,
+        ipfs_path: &IpfsPath,
+    ) -> LocalBoxStream<'_, Result<(RelativePathBuf, OutType)>> {
+        tracing::debug!("get {:?}", ipfs_path);
+        let resolver = iroh_resolver::resolver::Resolver::new(self.client.clone());
+        let results = resolver.resolve_recursive_with_paths(ipfs_path.clone());
+        let sub_path = ipfs_path.to_relative_string();
+        async_stream::try_stream! {
+            tokio::pin!(results);
+            while let Some(res) = results.next().await {
+                let (relative_ipfs_path, out) = res?;
+                let relative_path = RelativePathBuf::from_path(&relative_ipfs_path.to_relative_string())?;
+                // TODO(faassen) this focusing in on sub-paths should really be handled in the resolver:
+                // * it can be tested there far more easily than here (where currently it isn't)
+                // * it makes sense to have an API "what does this resolve to" in the resolver
+                // * the resolver may have opportunities for optimization we don't have
+                if !relative_path.starts_with(&sub_path) {
+                    continue;
                 }
-                OutType::Reader(mut reader) => {
-                    if let Some(parent) = path.parent() {
-                        tokio::fs::create_dir_all(parent).await?;
-                    }
-                    let mut f = tokio::fs::File::create(path).await?;
-                    tokio::io::copy(&mut reader, &mut f).await?;
+                let relative_path = relative_path.strip_prefix(&sub_path).expect("should be a prefix").to_owned();
+                if out.is_dir() {
+                    yield (relative_path, OutType::Dir);
+                } else {
+                    let reader = out.pretty(resolver.clone(), Default::default())?;
+                    yield (relative_path, OutType::Reader(Box::new(reader)));
                 }
             }
         }
-        Ok(())
+        .boxed_local()
     }
 
-    async fn add(&self, path: &Path, recursive: bool, no_wrap: bool) -> Result<Cid> {
-        let providing_client = iroh_resolver::unixfs_builder::StoreAndProvideClient {
-            client: Box::new(self.get_client()),
-        };
-        if path.is_dir() {
-            unixfs_builder::add_dir(Some(&providing_client), path, !no_wrap, recursive).await
-        } else if path.is_file() {
-            unixfs_builder::add_file(Some(&providing_client), path, !no_wrap).await
-        } else {
-            anyhow::bail!("can only add files or directories");
+    fn add<'a>(
+        &'a self,
+        path: &'a Path,
+        recursive: bool,
+        no_wrap: bool,
+    ) -> LocalBoxFuture<'_, Result<Cid>> {
+        async move {
+            let providing_client = iroh_resolver::unixfs_builder::StoreAndProvideClient {
+                client: Box::new(&self.client),
+            };
+            if path.is_dir() {
+                unixfs_builder::add_dir(Some(&providing_client), path, !no_wrap, recursive).await
+            } else if path.is_file() {
+                unixfs_builder::add_file(Some(&providing_client), path, !no_wrap).await
+            } else {
+                anyhow::bail!("can only add files or directories");
+            }
         }
+        .boxed_local()
     }
 
-    async fn check(&self) -> StatusTable {
-        self.client.check().await
+    fn check(&self) -> BoxFuture<'_, StatusTable> {
+        async { self.client.check().await }.boxed()
     }
 
-    async fn watch<'b>(&self) -> LocalBoxStream<'b, iroh_rpc_client::StatusTable> {
-        self.client.clone().watch().await.boxed()
+    fn watch(
+        &self,
+    ) -> LocalBoxFuture<'static, LocalBoxStream<'static, iroh_rpc_client::StatusTable>> {
+        let client = self.client.clone();
+        async { client.watch().await.boxed_local() }.boxed_local()
     }
 }
diff --git a/iroh-api/src/api_ext.rs b/iroh-api/src/api_ext.rs
new file mode 100644
index 00000000000..f82b9cc3492
--- /dev/null
+++ b/iroh-api/src/api_ext.rs
@@ -0,0 +1,124 @@
+use std::path::{Path, PathBuf};
+
+use crate::{Api, IpfsPath, OutType};
+use anyhow::{anyhow, Result};
+use async_trait::async_trait;
+use futures::Stream;
+use futures::StreamExt;
+use relative_path::RelativePathBuf;
+
+#[async_trait(?Send)]
+pub trait ApiExt: Api {
+    /// High level get, equivalent of CLI `iroh get`
+    async fn get<'a>(
+        &self,
+        ipfs_path: &IpfsPath,
+        output_path: Option<&'a Path>,
+    ) -> Result<PathBuf> {
+        if ipfs_path.cid().is_none() {
+            return Err(anyhow!("IPFS path does not refer to a CID"));
+        }
+        let root_path = get_root_path(ipfs_path, output_path);
+        if root_path.exists() {
+            return Err(anyhow!(
+                "output path {} already exists",
+                root_path.display()
+            ));
+        }
+        let blocks = self.get_stream(ipfs_path);
+        save_get_stream(&root_path, blocks).await?;
+        Ok(root_path)
+    }
+}
+
+impl<T> ApiExt for T where T: Api {}
+
+/// take a stream of blocks as from `get_stream` and write them to the filesystem
+async fn save_get_stream(
+    root_path: &Path,
+    blocks: impl Stream<Item = Result<(RelativePathBuf, OutType)>>,
+) -> Result<()> {
+    tokio::pin!(blocks);
+    while let Some(block) = blocks.next().await {
+        let (path, out) = block?;
+        let full_path = path.to_path(root_path);
+        match out {
+            OutType::Dir => {
+                tokio::fs::create_dir_all(full_path).await?;
+            }
+            OutType::Reader(mut reader) => {
+                if let Some(parent) = path.parent() {
+                    tokio::fs::create_dir_all(parent.to_path(root_path)).await?;
+                }
+                let mut f = tokio::fs::File::create(full_path).await?;
+                tokio::io::copy(&mut reader, &mut f).await?;
+            }
+        }
+    }
+    Ok(())
+}
+
+/// Given an cid and an optional output path, determine root path
+fn get_root_path(ipfs_path: &IpfsPath, output_path: Option<&Path>) -> PathBuf {
+    match output_path {
+        Some(path) => path.to_path_buf(),
+        None => {
+            if ipfs_path.tail().is_empty() {
+                PathBuf::from(ipfs_path.cid().unwrap().to_string())
+            } else {
+                PathBuf::from(ipfs_path.tail().last().unwrap())
+            }
+        }
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use std::str::FromStr;
+    use tempdir::TempDir;
+
+    #[tokio::test]
+    async fn test_save_get_stream() {
+        let stream = Box::pin(futures::stream::iter(vec![
+            Ok((RelativePathBuf::from_path("a").unwrap(), OutType::Dir)),
+            Ok((
+                RelativePathBuf::from_path("b").unwrap(),
+                OutType::Reader(Box::new(std::io::Cursor::new("hello"))),
+            )),
+        ]));
+        let tmp_dir = TempDir::new("test_save_get_stream").unwrap();
+        save_get_stream(tmp_dir.path(), stream).await.unwrap();
+        assert!(tmp_dir.path().join("a").is_dir());
+        assert_eq!(
+            std::fs::read_to_string(tmp_dir.path().join("b")).unwrap(),
+            "hello"
+        );
+    }
+
+    #[test]
+    fn test_get_root_path() {
+        let ipfs_path =
+            IpfsPath::from_str("/ipfs/QmYyQSo1c1Ym7orWxLYvCrM2EmxFTANf8wXmmE7DWjhx5N").unwrap();
+        assert_eq!(
+            get_root_path(&ipfs_path, None),
+            PathBuf::from("QmYyQSo1c1Ym7orWxLYvCrM2EmxFTANf8wXmmE7DWjhx5N")
+        );
+        assert_eq!(
+            get_root_path(&ipfs_path, Some(Path::new("bar"))),
+            PathBuf::from("bar")
+        );
+    }
+
+    #[test]
+    fn test_get_root_path_with_tail() {
+        let ipfs_path =
+            IpfsPath::from_str("/ipfs/QmYyQSo1c1Ym7orWxLYvCrM2EmxFTANf8wXmmE7DWjhx5N/tail")
+                .unwrap();
+        assert_eq!(get_root_path(&ipfs_path, None), PathBuf::from("tail"));
+        assert_eq!(
+            get_root_path(&ipfs_path, Some(Path::new("bar"))),
+            PathBuf::from("bar")
+        );
+    }
+}
diff --git a/iroh-api/src/getadd.rs b/iroh-api/src/getadd.rs
deleted file mode 100644
index 1504254bd18..00000000000
--- a/iroh-api/src/getadd.rs
+++ /dev/null
@@ -1,95 +0,0 @@
-use std::path::{Path, PathBuf};
-
-use crate::api::{Iroh, OutType};
-use anyhow::{Context, Result};
-use futures::Stream;
-use futures::StreamExt;
-use iroh_resolver::resolver::Path as IpfsPath;
-use iroh_rpc_client::Client;
-
-impl Iroh {
-    pub fn get_stream<'b>(
-        &self,
-        root: &'b IpfsPath,
-        output: Option<&'b Path>,
-    ) -> impl Stream<Item = Result<(PathBuf, OutType<Client>)>> + 'b {
-        tracing::debug!("get {:?}", root);
-        let resolver = iroh_resolver::resolver::Resolver::new(self.get_client().clone());
-        let results = resolver.resolve_recursive_with_paths(root.clone());
-        async_stream::try_stream! {
-            tokio::pin!(results);
-            while let Some(res) = results.next().await {
-                let (path, out) = res?;
-                let path = Self::make_output_path(path, root.clone(), output.clone())?;
-                if out.is_dir() {
-                    yield (path, OutType::Dir);
-                } else {
-                    let reader = out.pretty(resolver.clone(), Default::default())?;
-                    yield (path, OutType::Reader(reader));
-                }
-            }
-        }
-    }
-
-    /// Adjusts the full path to replace the root with any given output path
-    /// if it exists.
-    fn make_output_path(full: IpfsPath, root: IpfsPath, output: Option<&Path>) -> Result<PathBuf> {
-        if let Some(output) = output {
-            let root_str = &root.to_string()[..];
-            let full_as_path = PathBuf::from(full.to_string());
-            let path_str = full_as_path.to_str().context("invalid root path")?;
-            let output_str = output.to_str().context("invalid output path")?;
-            Ok(PathBuf::from(path_str.replace(root_str, output_str)))
-        } else {
-            // returns path as a string
-            Ok(PathBuf::from(full.to_string_without_type()))
-        }
-    }
-}
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-    use iroh_resolver::resolver;
-    use std::str::FromStr;
-
-    #[test]
-    fn test_make_output_path() {
-        // test with output dir
-        let root =
-            IpfsPath::from_str("/ipfs/QmYbcW4tXLXHWw753boCK8Y7uxLu5abXjyYizhLznq9PUR").unwrap();
-        let full =
-            IpfsPath::from_str("/ipfs/QmYbcW4tXLXHWw753boCK8Y7uxLu5abXjyYizhLznq9PUR/bar.txt")
-                .unwrap();
-        let output = Some(PathBuf::from("foo"));
-        let expect = PathBuf::from("foo/bar.txt");
-        let got = Iroh::make_output_path(full, root, output.as_deref()).unwrap();
-        assert_eq!(expect, got);
-
-        // test with output filepath
-        let root = resolver::Path::from_str(
-            "/ipfs/QmYbcW4tXLXHWw753boCK8Y7uxLu5abXjyYizhLznq9PUR/bar.txt",
-        )
-        .unwrap();
-        let full = resolver::Path::from_str(
-            "/ipfs/QmYbcW4tXLXHWw753boCK8Y7uxLu5abXjyYizhLznq9PUR/bar.txt",
-        )
-        .unwrap();
-        let output = Some(PathBuf::from("foo/baz.txt"));
-        let expect = PathBuf::from("foo/baz.txt");
-        let got = Iroh::make_output_path(full, root, output.as_deref()).unwrap();
-        assert_eq!(expect, got);
-
-        // test no output path
-        let root = resolver::Path::from_str("/ipfs/QmYbcW4tXLXHWw753boCK8Y7uxLu5abXjyYizhLznq9PUR")
-            .unwrap();
-        let full = resolver::Path::from_str(
-            "/ipfs/QmYbcW4tXLXHWw753boCK8Y7uxLu5abXjyYizhLznq9PUR/bar.txt",
-        )
-        .unwrap();
-        let output = None;
-        let expect = PathBuf::from("QmYbcW4tXLXHWw753boCK8Y7uxLu5abXjyYizhLznq9PUR/bar.txt");
-        let got = Iroh::make_output_path(full, root, output).unwrap();
-        assert_eq!(expect, got);
-    }
-}
diff --git a/iroh-api/src/lib.rs b/iroh-api/src/lib.rs
index 34fd5b912b6..d4d2db03bce 100644
--- a/iroh-api/src/lib.rs
+++ b/iroh-api/src/lib.rs
@@ -1,18 +1,19 @@
 mod api;
+mod api_ext;
 mod config;
-mod getadd;
 mod p2p;
 
 #[cfg(feature = "testing")]
 pub use crate::api::MockApi;
-pub use crate::api::{Api, Iroh};
+pub use crate::api::{Api, Iroh, OutType};
+pub use crate::api_ext::ApiExt;
 #[cfg(feature = "testing")]
 pub use crate::p2p::MockP2p;
 pub use crate::p2p::P2p as P2pApi;
 pub use crate::p2p::{Lookup, PeerIdOrAddr};
 pub use bytes::Bytes;
 pub use cid::Cid;
-pub use iroh_resolver::resolver::{CidOrDomain, Path as IpfsPath};
+pub use iroh_resolver::resolver::Path as IpfsPath;
 pub use iroh_rpc_client::{ServiceStatus, StatusRow, StatusTable};
 pub use libp2p::gossipsub::MessageId;
 pub use libp2p::{Multiaddr, PeerId};
diff --git a/iroh-resolver/src/resolver.rs b/iroh-resolver/src/resolver.rs
index 5318138dd8c..d150432fff5 100644
--- a/iroh-resolver/src/resolver.rs
+++ b/iroh-resolver/src/resolver.rs
@@ -119,18 +119,20 @@ impl Path {
         self.tail.push(str.as_ref().to_owned());
     }
 
-    pub fn to_string_without_type(&self) -> String {
-        let mut s = format!("{}", self.root);
-        for part in &self.tail {
-            if part.is_empty() {
-                continue;
-            }
-            s.push_str(&format!("/{}", part)[..]);
-        }
-        if self.has_trailing_slash() {
-            s.push('/');
+    // Empty path segments in the *middle* shouldn't occur,
+    // though they can occur at the end, which `join` handles.
+    // TODO(faassen): it would make sense to return a `RelativePathBuf` here at some
+    // point in the future so we don't deal with bare strings anymore and
+    // we're forced to handle various cases more explicitly.
+    pub fn to_relative_string(&self) -> String {
+        self.tail.join("/")
+    }
+
+    pub fn cid(&self) -> Option<&Cid> {
+        match &self.root {
+            CidOrDomain::Cid(cid) => Some(cid),
+            CidOrDomain::Domain(_) => None,
         }
-        s
     }
 }
 
diff --git a/iroh/Cargo.toml b/iroh/Cargo.toml
index 69dcf07d3ed..a5e7da2ebc6 100644
--- a/iroh/Cargo.toml
+++ b/iroh/Cargo.toml
@@ -8,19 +8,20 @@ repository = "https://github.com/n0-computer/iroh"
 description = "Command line interface for interacting with iroh."
 
 [features]
-testing = []
+testing = ["dep:relative-path"]
 
 [dependencies]
 anyhow = "1.0"
 futures = "0.3.21"
 tokio = { version = "1", features = ["fs", "io-util"] }
 tracing = "0.1.34"
-clap = { version = "4.0.9", features = ["derive"] }
+clap = { version = "4.0.15", features = ["derive"] }
 crossterm = "0.25"
 tonic = "0.8"
 git-version = "0.3.5"
 iroh-metrics = { path = "../iroh-metrics", default-features = false, features = ["rpc-grpc"] }
 iroh-api = { path = "../iroh-api"}
+relative-path = { version = "1.7.2", optional = true }
 
 [dev-dependencies]
 trycmd = "0.13.7"
diff --git a/iroh/src/fixture.rs b/iroh/src/fixture.rs
index 07e8c814ca6..41c4a770f35 100644
--- a/iroh/src/fixture.rs
+++ b/iroh/src/fixture.rs
@@ -1,7 +1,9 @@
 use std::collections::HashMap;
 use std::env;
 
-use iroh_api::{Lookup, MockApi, MockP2p, PeerId};
+use futures::StreamExt;
+use iroh_api::{Lookup, MockApi, MockP2p, OutType, PeerId};
+use relative_path::RelativePathBuf;
 
 type GetFixture = fn() -> MockApi;
 type FixtureRegistry = HashMap<String, GetFixture>;
@@ -28,7 +30,51 @@ fn fixture_lookup() -> MockApi {
 
 fn fixture_get() -> MockApi {
     let mut api = MockApi::default();
-    api.expect_get().returning(|_, _| Ok(()));
+    api.expect_get_stream().returning(|_ipfs_path| {
+        futures::stream::iter(vec![
+            Ok((RelativePathBuf::from_path("").unwrap(), OutType::Dir)),
+            Ok((RelativePathBuf::from_path("a").unwrap(), OutType::Dir)),
+            // git doesn't like empty directories, nor does trycmd trip if it's missing
+            // we rely on the unit test for save_get_stream elsewhere to check empty
+            // directories are created
+            Ok((
+                RelativePathBuf::from_path("a/exists").unwrap(),
+                OutType::Reader(Box::new(std::io::Cursor::new("exists"))),
+            )),
+            Ok((
+                RelativePathBuf::from_path("b").unwrap(),
+                OutType::Reader(Box::new(std::io::Cursor::new("hello"))),
+            )),
+        ])
+        .boxed_local()
+    });
+    api
+}
+
+fn fixture_get_wrapped_file() -> MockApi {
+    let mut api = MockApi::default();
+    api.expect_get_stream().returning(|_ipfs_path| {
+        futures::stream::iter(vec![
+            Ok((RelativePathBuf::from_path("").unwrap(), OutType::Dir)),
+            Ok((
+                RelativePathBuf::from_path("file.txt").unwrap(),
+                OutType::Reader(Box::new(std::io::Cursor::new("hello"))),
+            )),
+        ])
+        .boxed_local()
+    });
+    api
+}
+
+fn fixture_get_unwrapped_file() -> MockApi {
+    let mut api = MockApi::default();
+    api.expect_get_stream().returning(|_ipfs_path| {
+        futures::stream::iter(vec![Ok((
+            RelativePathBuf::from_path("").unwrap(),
+            OutType::Reader(Box::new(std::io::Cursor::new("hello"))),
+        ))])
+        .boxed_local()
+    });
     api
 }
 
@@ -36,6 +82,14 @@ fn register_fixtures() -> FixtureRegistry {
     [
         ("lookup".to_string(), fixture_lookup as GetFixture),
         ("get".to_string(), fixture_get as GetFixture),
+        (
+            "get_wrapped_file".to_string(),
+            fixture_get_wrapped_file as GetFixture,
+        ),
+        (
+            "get_unwrapped_file".to_string(),
+            fixture_get_unwrapped_file as GetFixture,
+        ),
     ]
     .into_iter()
     .collect()
diff --git a/iroh/src/run.rs b/iroh/src/run.rs
index 2ba844dbbe3..1f7f32cddfa 100644
--- a/iroh/src/run.rs
+++ b/iroh/src/run.rs
@@ -6,7 +6,7 @@ use crate::fixture::get_fixture_api;
 use crate::p2p::{run_command as run_p2p_command, P2p};
 use anyhow::Result;
 use clap::{Parser, Subcommand};
-use iroh_api::{Api, CidOrDomain, IpfsPath, Iroh};
+use iroh_api::{Api, ApiExt, IpfsPath, Iroh};
 use iroh_metrics::config::Config as MetricsConfig;
 
 #[derive(Parser, Debug, Clone)]
@@ -52,9 +52,9 @@ enum Commands {
     )]
     Get {
         /// CID or CID/with/path/qualifier to get
-        path: IpfsPath,
+        ipfs_path: IpfsPath,
         /// filesystem path to write to. Defaults to CID
-        output: Option<PathBuf>,
+        output_path: Option<PathBuf>,
     },
 }
 
@@ -109,18 +109,12 @@ impl Cli {
                 let cid = api.add(path, *recursive, *no_wrap).await?;
                 println!("/ipfs/{}", cid);
             }
-            Commands::Get { path, output } => {
-                let cid = if let CidOrDomain::Cid(cid) = path.root() {
-                    cid
-                } else {
-                    return Err(anyhow::anyhow!("ipfs path must refer to a CID"));
-                };
-                api.get(path, output.as_deref()).await?;
-                let real_output = output
-                    .as_deref()
-                    .map(|path| path.to_path_buf())
-                    .unwrap_or_else(|| PathBuf::from(&cid.to_string()));
-                println!("Saving file(s) to {}", real_output.to_str().unwrap());
+            Commands::Get {
+                ipfs_path,
+                output_path,
+            } => {
+                let root_path = api.get(ipfs_path, output_path.as_deref()).await?;
+                println!("Saving file(s) to {}", root_path.to_str().unwrap());
             }
         };
 
diff --git a/iroh/tests/cli_tests.rs b/iroh/tests/cli_tests.rs
index 9b9077d1c1c..8ee84ba651f 100644
--- a/iroh/tests/cli_tests.rs
+++ b/iroh/tests/cli_tests.rs
@@ -1,5 +1,8 @@
+// using globs for trycmd unfortunately leads to some issues
+// when `.in` and `.out` directories are in use. So we avoid that
+
 #[tokio::test]
-async fn lookup_cli_test() {
+async fn lookup_test() {
     trycmd::TestCases::new()
         .env("IROH_CTL_FIXTURE", "lookup")
         .case("tests/cmd/lookup.trycmd")
@@ -7,10 +10,84 @@ async fn lookup_cli_test() {
 }
 
 #[tokio::test]
-async fn get_cli_test() {
+async fn get_success_cid_explicit_output_path_success_test() {
+    trycmd::TestCases::new()
+        .env("IROH_CTL_FIXTURE", "get")
+        .case("tests/cmd/get_cid_explicit_output_path_success.trycmd")
+        .run();
+}
+
+#[tokio::test]
+async fn get_cid_success_test() {
+    trycmd::TestCases::new()
+        .env("IROH_CTL_FIXTURE", "get")
+        .case("tests/cmd/get_cid_success.trycmd")
+        .run();
+}
+
+#[tokio::test]
+async fn get_ipfs_path_success_test() {
+    trycmd::TestCases::new()
+        .env("IROH_CTL_FIXTURE", "get")
+        .case("tests/cmd/get_ipfs_path_success.trycmd")
+        .run();
+}
+
+#[tokio::test]
+async fn get_tail_success_test() {
+    // we use the get_unwrapped_file fixture because it delivers a file
+    // which is what the test simulates
+    trycmd::TestCases::new()
+        .env("IROH_CTL_FIXTURE", "get_unwrapped_file")
+        .case("tests/cmd/get_tail_success.trycmd")
+        .run();
+}
+
+#[tokio::test]
+async fn get_cid_directory_overwrite_expicit_failure_test() {
+    trycmd::TestCases::new()
+        .env("IROH_CTL_FIXTURE", "get")
+        .case("tests/cmd/get_cid_directory_overwrite_explicit_failure.trycmd")
+        .run();
+}
+
+#[tokio::test]
+async fn get_cid_directory_overwrite_failure_test() {
+    trycmd::TestCases::new()
+        .env("IROH_CTL_FIXTURE", "get")
+        .case("tests/cmd/get_cid_directory_overwrite_failure.trycmd")
+        .run();
+}
+
+#[tokio::test]
+async fn get_wrapped_file_test() {
+    trycmd::TestCases::new()
+        .env("IROH_CTL_FIXTURE", "get_wrapped_file")
+        .case("tests/cmd/get_wrapped_file.trycmd")
+        .run();
+}
+
+#[tokio::test]
+async fn get_unwrapped_file_test() {
+    trycmd::TestCases::new()
+        .env("IROH_CTL_FIXTURE", "get_unwrapped_file")
+        .case("tests/cmd/get_unwrapped_file.trycmd")
+        .run();
+}
+
+#[tokio::test]
+async fn get_unwrapped_file_overwrite_test() {
+    trycmd::TestCases::new()
+        .env("IROH_CTL_FIXTURE", "get_unwrapped_file")
+        .case("tests/cmd/get_unwrapped_file_overwrite.trycmd")
+        .run();
+}
+
+#[tokio::test]
+async fn get_failure_test() {
     trycmd::TestCases::new()
         .env("IROH_CTL_FIXTURE", "get")
-        .case("tests/cmd/get.trycmd")
+        .case("tests/cmd/get_failure.trycmd")
         .run();
 }
 
diff --git a/iroh/tests/cmd/get.trycmd b/iroh/tests/cmd/get.trycmd
deleted file mode 100644
index dee08b4336f..00000000000
--- a/iroh/tests/cmd/get.trycmd
+++ /dev/null
@@ -1,16 +0,0 @@
-We can get a CID:
-
-```
-$ iroh get QmP8jTG1m9GSDJLCbeWhVSVgEzCPPwXRdCRuJtQ5Tz9Kc9
-Saving file(s) to QmP8jTG1m9GSDJLCbeWhVSVgEzCPPwXRdCRuJtQ5Tz9Kc9
-
-```
-
-We can also get a ipfs path:
-
-
-```
-$ iroh get /ipfs/QmP8jTG1m9GSDJLCbeWhVSVgEzCPPwXRdCRuJtQ5Tz9Kc9
-Saving file(s) to QmP8jTG1m9GSDJLCbeWhVSVgEzCPPwXRdCRuJtQ5Tz9Kc9
-
-```
diff --git a/iroh/tests/cmd/get_cid_directory_overwrite_explicit_failure.in/explicit/README.md b/iroh/tests/cmd/get_cid_directory_overwrite_explicit_failure.in/explicit/README.md
new file mode 100644
index 00000000000..79677091f82
--- /dev/null
+++ b/iroh/tests/cmd/get_cid_directory_overwrite_explicit_failure.in/explicit/README.md
@@ -0,0 +1 @@
+This is just to have something here. Overwriting this should fail
diff --git a/iroh/tests/cmd/get_cid_directory_overwrite_explicit_failure.out/explicit/README.md b/iroh/tests/cmd/get_cid_directory_overwrite_explicit_failure.out/explicit/README.md
new file mode 100644
index 00000000000..79677091f82
--- /dev/null
+++ b/iroh/tests/cmd/get_cid_directory_overwrite_explicit_failure.out/explicit/README.md
@@ -0,0 +1 @@
+This is just to have something here. Overwriting this should fail
diff --git a/iroh/tests/cmd/get_cid_directory_overwrite_explicit_failure.trycmd b/iroh/tests/cmd/get_cid_directory_overwrite_explicit_failure.trycmd
new file mode 100644
index 00000000000..dd1367f7012
--- /dev/null
+++ b/iroh/tests/cmd/get_cid_directory_overwrite_explicit_failure.trycmd
@@ -0,0 +1,10 @@
+
+We can get a CID that represents a directory, but this directory already
+exists, so we refuse to write:
+
+```
+$ iroh get QmP8jTG1m9GSDJLCbeWhVSVgEzCPPwXRdCRuJtQ5Tz9Kc9 explicit
+? failed
+Error: output path explicit already exists
+
+```
diff --git a/iroh/tests/cmd/get_cid_directory_overwrite_failure.in/QmP8jTG1m9GSDJLCbeWhVSVgEzCPPwXRdCRuJtQ5Tz9Kc9/README.md b/iroh/tests/cmd/get_cid_directory_overwrite_failure.in/QmP8jTG1m9GSDJLCbeWhVSVgEzCPPwXRdCRuJtQ5Tz9Kc9/README.md
new file mode 100644
index 00000000000..79677091f82
--- /dev/null
+++ b/iroh/tests/cmd/get_cid_directory_overwrite_failure.in/QmP8jTG1m9GSDJLCbeWhVSVgEzCPPwXRdCRuJtQ5Tz9Kc9/README.md
@@ -0,0 +1 @@
+This is just to have something here. Overwriting this should fail
diff --git a/iroh/tests/cmd/get_cid_directory_overwrite_failure.out/QmP8jTG1m9GSDJLCbeWhVSVgEzCPPwXRdCRuJtQ5Tz9Kc9/README.md b/iroh/tests/cmd/get_cid_directory_overwrite_failure.out/QmP8jTG1m9GSDJLCbeWhVSVgEzCPPwXRdCRuJtQ5Tz9Kc9/README.md
new file mode 100644
index 00000000000..79677091f82
--- /dev/null
+++ b/iroh/tests/cmd/get_cid_directory_overwrite_failure.out/QmP8jTG1m9GSDJLCbeWhVSVgEzCPPwXRdCRuJtQ5Tz9Kc9/README.md
@@ -0,0 +1 @@
+This is just to have something here. Overwriting this should fail
diff --git a/iroh/tests/cmd/get_cid_directory_overwrite_failure.trycmd b/iroh/tests/cmd/get_cid_directory_overwrite_failure.trycmd
new file mode 100644
index 00000000000..0e4ce524857
--- /dev/null
+++ b/iroh/tests/cmd/get_cid_directory_overwrite_failure.trycmd
@@ -0,0 +1,10 @@
+
+We can get a CID that represents a directory, but this directory already
+exists, so we refuse to write:
+
+```
+$ iroh get QmP8jTG1m9GSDJLCbeWhVSVgEzCPPwXRdCRuJtQ5Tz9Kc9
+? failed
+Error: output path QmP8jTG1m9GSDJLCbeWhVSVgEzCPPwXRdCRuJtQ5Tz9Kc9 already exists
+
+```
diff --git a/iroh/tests/cmd/get_cid_explicit_output_path_success.out/explicit/a/exists b/iroh/tests/cmd/get_cid_explicit_output_path_success.out/explicit/a/exists
new file mode 100644
index 00000000000..01cd34b88db
--- /dev/null
+++ b/iroh/tests/cmd/get_cid_explicit_output_path_success.out/explicit/a/exists
@@ -0,0 +1 @@
+exists
\ No newline at end of file
diff --git a/iroh/tests/cmd/get_cid_explicit_output_path_success.out/explicit/b b/iroh/tests/cmd/get_cid_explicit_output_path_success.out/explicit/b
new file mode 100644
index 00000000000..b6fc4c620b6
--- /dev/null
+++ b/iroh/tests/cmd/get_cid_explicit_output_path_success.out/explicit/b
@@ -0,0 +1 @@
+hello
\ No newline at end of file
diff --git a/iroh/tests/cmd/get_cid_explicit_output_path_success.trycmd b/iroh/tests/cmd/get_cid_explicit_output_path_success.trycmd
new file mode 100644
index 00000000000..5a6ade533f0
--- /dev/null
+++ b/iroh/tests/cmd/get_cid_explicit_output_path_success.trycmd
@@ -0,0 +1,8 @@
+
+We can get a CID and define an explicit output path:
+
+```
+$ iroh get QmP8jTG1m9GSDJLCbeWhVSVgEzCPPwXRdCRuJtQ5Tz9Kc9 explicit
+Saving file(s) to explicit
+
+```
\ No newline at end of file
diff --git a/iroh/tests/cmd/get_cid_success.out/QmP8jTG1m9GSDJLCbeWhVSVgEzCPPwXRdCRuJtQ5Tz9Kc9/a/exists b/iroh/tests/cmd/get_cid_success.out/QmP8jTG1m9GSDJLCbeWhVSVgEzCPPwXRdCRuJtQ5Tz9Kc9/a/exists
new file mode 100644
index 00000000000..01cd34b88db
--- /dev/null
+++ b/iroh/tests/cmd/get_cid_success.out/QmP8jTG1m9GSDJLCbeWhVSVgEzCPPwXRdCRuJtQ5Tz9Kc9/a/exists
@@ -0,0 +1 @@
+exists
\ No newline at end of file
diff --git a/iroh/tests/cmd/get_cid_success.out/QmP8jTG1m9GSDJLCbeWhVSVgEzCPPwXRdCRuJtQ5Tz9Kc9/b b/iroh/tests/cmd/get_cid_success.out/QmP8jTG1m9GSDJLCbeWhVSVgEzCPPwXRdCRuJtQ5Tz9Kc9/b
new file mode 100644
index 00000000000..b6fc4c620b6
--- /dev/null
+++ b/iroh/tests/cmd/get_cid_success.out/QmP8jTG1m9GSDJLCbeWhVSVgEzCPPwXRdCRuJtQ5Tz9Kc9/b
@@ -0,0 +1 @@
+hello
\ No newline at end of file
diff --git a/iroh/tests/cmd/get_cid_success.trycmd b/iroh/tests/cmd/get_cid_success.trycmd
new file mode 100644
index 00000000000..1d8f71fa80d
--- /dev/null
+++ b/iroh/tests/cmd/get_cid_success.trycmd
@@ -0,0 +1,8 @@
+
+We can get a CID:
+
+```
+$ iroh get QmP8jTG1m9GSDJLCbeWhVSVgEzCPPwXRdCRuJtQ5Tz9Kc9
+Saving file(s) to QmP8jTG1m9GSDJLCbeWhVSVgEzCPPwXRdCRuJtQ5Tz9Kc9
+
+```
diff --git a/iroh/tests/cmd/get_failure.trycmd b/iroh/tests/cmd/get_failure.trycmd
new file mode 100644
index 00000000000..a80d4984f93
--- /dev/null
+++ b/iroh/tests/cmd/get_failure.trycmd
@@ -0,0 +1,31 @@
+Getting something that's not a proper path to a CID fails with an error:
+
+```
+$ iroh get /something
+? failed
+error: Invalid value '/something' for '<IPFS_PATH>': invalid cid
+
+For more information try '--help'
+
+```
+
+
+Getting something that's not a CID fails too:
+
+```
+$ iroh get QmP8jTG1m9GSDJLCbeWhVSVg
+? failed
+error: Invalid value 'QmP8jTG1m9GSDJLCbeWhVSVg' for '<IPFS_PATH>': invalid cid
+
+For more information try '--help'
+
+```
+
+Getting something that's a proper path but does not include a CID is also an error:
+
+```
+$ iroh get /ipns/foo
+? failed
+Error: IPFS path does not refer to a CID
+
+```
\ No newline at end of file
diff --git a/iroh/tests/cmd/get_ipfs_path_success.out/QmP8jTG1m9GSDJLCbeWhVSVgEzCPPwXRdCRuJtQ5Tz9Kc9/a/exists b/iroh/tests/cmd/get_ipfs_path_success.out/QmP8jTG1m9GSDJLCbeWhVSVgEzCPPwXRdCRuJtQ5Tz9Kc9/a/exists
new file mode 100644
index 00000000000..01cd34b88db
--- /dev/null
+++ b/iroh/tests/cmd/get_ipfs_path_success.out/QmP8jTG1m9GSDJLCbeWhVSVgEzCPPwXRdCRuJtQ5Tz9Kc9/a/exists
@@ -0,0 +1 @@
+exists
\ No newline at end of file
diff --git a/iroh/tests/cmd/get_ipfs_path_success.out/QmP8jTG1m9GSDJLCbeWhVSVgEzCPPwXRdCRuJtQ5Tz9Kc9/b b/iroh/tests/cmd/get_ipfs_path_success.out/QmP8jTG1m9GSDJLCbeWhVSVgEzCPPwXRdCRuJtQ5Tz9Kc9/b
new file mode 100644
index 00000000000..b6fc4c620b6
--- /dev/null
+++ b/iroh/tests/cmd/get_ipfs_path_success.out/QmP8jTG1m9GSDJLCbeWhVSVgEzCPPwXRdCRuJtQ5Tz9Kc9/b
@@ -0,0 +1 @@
+hello
\ No newline at end of file
diff --git a/iroh/tests/cmd/get_ipfs_path_success.trycmd b/iroh/tests/cmd/get_ipfs_path_success.trycmd
new file mode 100644
index 00000000000..89ff0e16d0c
--- /dev/null
+++ b/iroh/tests/cmd/get_ipfs_path_success.trycmd
@@ -0,0 +1,9 @@
+
+We can also get a ipfs path to a CID:
+
+
+```
+$ iroh get /ipfs/QmP8jTG1m9GSDJLCbeWhVSVgEzCPPwXRdCRuJtQ5Tz9Kc9
+Saving file(s) to QmP8jTG1m9GSDJLCbeWhVSVgEzCPPwXRdCRuJtQ5Tz9Kc9
+
+```
\ No newline at end of file
diff --git a/iroh/tests/cmd/get_tail_success.out/b b/iroh/tests/cmd/get_tail_success.out/b
new file mode 100644
index 00000000000..b6fc4c620b6
--- /dev/null
+++ b/iroh/tests/cmd/get_tail_success.out/b
@@ -0,0 +1 @@
+hello
\ No newline at end of file
diff --git a/iroh/tests/cmd/get_tail_success.trycmd b/iroh/tests/cmd/get_tail_success.trycmd
new file mode 100644
index 00000000000..0b7aefdd40d
--- /dev/null
+++ b/iroh/tests/cmd/get_tail_success.trycmd
@@ -0,0 +1,8 @@
+
+We can get a CID indicating a specific path inside:
+
+```
+$ iroh get /ipfs/QmP8jTG1m9GSDJLCbeWhVSVgEzCPPwXRdCRuJtQ5Tz9Kc9/b
+Saving file(s) to b
+
+```
diff --git a/iroh/tests/cmd/get_unwrapped_file.out/QmP8jTG1m9GSDJLCbeWhVSVgEzCPPwXRdCRuJtQ5Tz9Kc9 b/iroh/tests/cmd/get_unwrapped_file.out/QmP8jTG1m9GSDJLCbeWhVSVgEzCPPwXRdCRuJtQ5Tz9Kc9
new file mode 100644
index 00000000000..b6fc4c620b6
--- /dev/null
+++ b/iroh/tests/cmd/get_unwrapped_file.out/QmP8jTG1m9GSDJLCbeWhVSVgEzCPPwXRdCRuJtQ5Tz9Kc9
@@ -0,0 +1 @@
+hello
\ No newline at end of file
diff --git a/iroh/tests/cmd/get_unwrapped_file.trycmd b/iroh/tests/cmd/get_unwrapped_file.trycmd
new file mode 100644
index 00000000000..7dd6ed5c9b6
--- /dev/null
+++ b/iroh/tests/cmd/get_unwrapped_file.trycmd
@@ -0,0 +1,7 @@
+Get a file that's not wrapped:
+
+```
+$ iroh get /ipfs/QmP8jTG1m9GSDJLCbeWhVSVgEzCPPwXRdCRuJtQ5Tz9Kc9
+Saving file(s) to QmP8jTG1m9GSDJLCbeWhVSVgEzCPPwXRdCRuJtQ5Tz9Kc9
+
+```
\ No newline at end of file
diff --git a/iroh/tests/cmd/get_unwrapped_file_overwrite.in/QmP8jTG1m9GSDJLCbeWhVSVgEzCPPwXRdCRuJtQ5Tz9Kc9 b/iroh/tests/cmd/get_unwrapped_file_overwrite.in/QmP8jTG1m9GSDJLCbeWhVSVgEzCPPwXRdCRuJtQ5Tz9Kc9
new file mode 100644
index 00000000000..5b0cfcb7abd
--- /dev/null
+++ b/iroh/tests/cmd/get_unwrapped_file_overwrite.in/QmP8jTG1m9GSDJLCbeWhVSVgEzCPPwXRdCRuJtQ5Tz9Kc9
@@ -0,0 +1 @@
+This won't be overwritten
\ No newline at end of file
diff --git a/iroh/tests/cmd/get_unwrapped_file_overwrite.out/QmP8jTG1m9GSDJLCbeWhVSVgEzCPPwXRdCRuJtQ5Tz9Kc9 b/iroh/tests/cmd/get_unwrapped_file_overwrite.out/QmP8jTG1m9GSDJLCbeWhVSVgEzCPPwXRdCRuJtQ5Tz9Kc9
new file mode 100644
index 00000000000..5b0cfcb7abd
--- /dev/null
+++ b/iroh/tests/cmd/get_unwrapped_file_overwrite.out/QmP8jTG1m9GSDJLCbeWhVSVgEzCPPwXRdCRuJtQ5Tz9Kc9
@@ -0,0 +1 @@
+This won't be overwritten
\ No newline at end of file
diff --git a/iroh/tests/cmd/get_unwrapped_file_overwrite.trycmd b/iroh/tests/cmd/get_unwrapped_file_overwrite.trycmd
new file mode 100644
index 00000000000..e463d06d5ed
--- /dev/null
+++ b/iroh/tests/cmd/get_unwrapped_file_overwrite.trycmd
@@ -0,0 +1,8 @@
+Get a file that's not wrapped. Overwriting an existing file fails:
+
+```
+$ iroh get /ipfs/QmP8jTG1m9GSDJLCbeWhVSVgEzCPPwXRdCRuJtQ5Tz9Kc9
+? failed
+Error: output path QmP8jTG1m9GSDJLCbeWhVSVgEzCPPwXRdCRuJtQ5Tz9Kc9 already exists
+
+```
\ No newline at end of file
diff --git a/iroh/tests/cmd/get_wrapped_file.out/QmP8jTG1m9GSDJLCbeWhVSVgEzCPPwXRdCRuJtQ5Tz9Kc9/file.txt b/iroh/tests/cmd/get_wrapped_file.out/QmP8jTG1m9GSDJLCbeWhVSVgEzCPPwXRdCRuJtQ5Tz9Kc9/file.txt
new file mode 100644
index 00000000000..b6fc4c620b6
--- /dev/null
+++ b/iroh/tests/cmd/get_wrapped_file.out/QmP8jTG1m9GSDJLCbeWhVSVgEzCPPwXRdCRuJtQ5Tz9Kc9/file.txt
@@ -0,0 +1 @@
+hello
\ No newline at end of file
diff --git a/iroh/tests/cmd/get_wrapped_file.trycmd b/iroh/tests/cmd/get_wrapped_file.trycmd
new file mode 100644
index 00000000000..913c97d3f3c
--- /dev/null
+++ b/iroh/tests/cmd/get_wrapped_file.trycmd
@@ -0,0 +1,7 @@
+Get a file wrapped into a directory:
+
+```
+$ iroh get /ipfs/QmP8jTG1m9GSDJLCbeWhVSVgEzCPPwXRdCRuJtQ5Tz9Kc9
+Saving file(s) to QmP8jTG1m9GSDJLCbeWhVSVgEzCPPwXRdCRuJtQ5Tz9Kc9
+
+```
\ No newline at end of file
