diff --git a/src/bin/zfs-tpm-list.cpp b/src/bin/zfs-tpm-list.cpp
new file mode 100644
index 0000000..aa421d8
--- /dev/null
+++ b/src/bin/zfs-tpm-list.cpp
@@ -0,0 +1,114 @@
+/* SPDX-License-Identifier: MIT */
+
+
+#include "../main.hpp"
+#include "../parse.hpp"
+#include "../zfs.hpp"
+
+#include <algorithm>
+#include <sys/mman.h>
+
+
+#define TZPFMS_BACKEND_MAX_LEN 16
+
+
+/// zfs(8) uses struct zprop_get_cbdata_t, which is powerful, but inscrutable; we have a fixed format, which makes this easier
+struct output_line {
+	char name[ZFS_MAX_DATASET_NAME_LEN + 1];
+	char backend[TZPFMS_BACKEND_MAX_LEN + 1];
+	bool key_available : 1;
+	bool coherent : 1;
+};
+
+
+int main(int argc, char ** argv) {
+	bool human           = true;
+	bool print_nontzpfms = false;
+	size_t maxdepth      = MAXDEPTH_UNSET;
+	return do_bare_main(
+	    argc, argv, "Hrd:a", "[-H] [-r|-d max] [-a]",
+	    [&](auto arg) {
+		    switch(arg) {
+			    case 'H':
+				    human = false;
+				    break;
+			    case 'r':
+				    maxdepth = SIZE_MAX;
+				    break;
+			    case 'd':
+				    if(parse_int(optarg, maxdepth)) {
+					    fprintf(stderr, "%s is not an integer\n", optarg);
+					    return __LINE__;
+				    }
+				    break;
+			    case 'a':
+				    print_nontzpfms = true;
+				    break;
+		    }
+		    return 0;
+	    },
+	    [&](auto libz) {
+		    output_line * lines{};
+		    size_t lines_len{};
+		    quickscope_wrapper lines_deleter{[&] { free(lines); }};
+
+
+		    TRY_MAIN(for_all_datasets(libz, argv + optind, maxdepth, [&](auto dataset) {
+			    boolean_t dataset_is_root;
+			    TRY("get encryption root", zfs_crypto_get_encryption_root(dataset, &dataset_is_root, nullptr));
+			    if(!dataset_is_root)
+				    return 0;
+
+			    char *backend{}, *handle{};
+			    TRY_MAIN(lookup_userprop(dataset, PROPNAME_BACKEND, backend));
+			    TRY_MAIN(lookup_userprop(dataset, PROPNAME_KEY, handle));
+
+			    ++lines_len;
+			    lines = TRY_PTR("allocate line buffer", reinterpret_cast<output_line *>(realloc(lines, sizeof(output_line) * lines_len)));
+
+			    auto & cur_line = lines[lines_len - 1];
+			    strncpy(cur_line.name, zfs_get_name(dataset), ZFS_MAX_DATASET_NAME_LEN);
+			    strncpy(cur_line.backend, (backend && strlen(backend) <= TZPFMS_BACKEND_MAX_LEN) ? backend : "\0", TZPFMS_BACKEND_MAX_LEN);
+			    // Tristate available/unavailable/none, but it's gonna be either available or unavailable on envryption roots, so
+			    cur_line.key_available = zfs_prop_get_int(dataset, ZFS_PROP_KEYSTATUS) == ZFS_KEYSTATUS_AVAILABLE;
+			    cur_line.coherent = !!backend == !!handle;
+
+			    return 0;
+		    }));
+
+		    auto max_name_len          = 0u;
+		    auto max_backend_len       = 0u;
+		    auto max_key_available_len = 0u;
+		    auto max_coherent_len      = 0u;
+		    auto separator             = "\t";
+		    if(human) {
+			    max_name_len          = strlen("NAME");
+			    max_backend_len       = strlen("BACK-END");
+			    max_key_available_len = strlen("KEYSTATUS");
+			    max_coherent_len      = strlen("COHERENT");
+			    separator             = "  ";
+
+			    for(auto cur = lines; cur != lines + lines_len; ++cur)
+				    if(print_nontzpfms || cur->backend[0] != '\0') {
+					    max_name_len          = std::max(max_name_len, strlen(cur->name));
+					    max_backend_len       = std::max(max_backend_len, (cur->backend[0] != '\0') ? strlen(cur->backend) : strlen("-"));
+					    max_key_available_len = std::max(max_key_available_len, cur->key_available ? strlen("available") : strlen("unavailable"));
+				    }
+		    }
+
+		    auto println = [&](auto name, auto backend, auto key_available, auto coherent) {
+			    printf("%-*s%s%-*s%s%-*s%s%-*s\n",                       //
+			           max_name_len, name, separator,                    //
+			           max_backend_len, backend, separator,              //
+			           max_key_available_len, key_available, separator,  //
+			           max_coherent_len, coherent);
+		    };
+		    if(human)
+			    println("NAME", "BACK-END", "KEYSTATUS", "COHERENT");
+		    for(auto cur = lines; cur != lines + lines_len; ++cur)
+			    if(print_nontzpfms || cur->backend[0] != '\0')
+				    println(cur->name, (cur->backend[0] != '\0') ? cur->backend : "-", cur->key_available ? "available" : "unavailable", cur->coherent ? "yes" : "no");
+
+		    return 0;
+	    });
+}
diff --git a/src/main.hpp b/src/main.hpp
index 79100e9..da7d05f 100644
--- a/src/main.hpp
+++ b/src/main.hpp
@@ -7,6 +7,7 @@
 #include "common.hpp"
 #include <libzfs.h>
 #include <stdlib.h>
+#include <type_traits>
 #include <unistd.h>
 
 
@@ -19,7 +20,7 @@
 
 
 template <class G, class M>
-int do_main(int argc, char ** argv, const char * getoptions, const char * usage, G && getoptfn, M && main) {
+int do_bare_main(int argc, char ** argv, const char * getoptions, const char * usage, G && getoptfn, M && main) {
 	const auto libz = TRY_PTR("initialise libzfs", libzfs_init());
 	quickscope_wrapper libz_deleter{[=] { libzfs_fini(libz); }};
 
@@ -40,35 +41,49 @@ int do_main(int argc, char ** argv, const char * getoptions, const char * usage,
 				printf("tzpfms version %s\n", TZPFMS_VERSION);
 				return 0;
 			default:
-				getoptfn(opt);
+				if constexpr(std::is_same_v<std::invoke_result_t<G, decltype(opt)>, void>)
+					getoptfn(opt);
+				else {
+					if(auto err = getoptfn(opt)) {
+						fprintf(stderr, "Usage: %s [-hV] %s%s<dataset>\n", argv[0], usage, strlen(usage) ? " " : "");
+						return err;
+					}
+				}
 		}
 
-	if(optind >= argc) {
-		fprintf(stderr,
-		        "No dataset to act on?\n"
-		        "Usage: %s [-hV] %s%s<dataset>\n",
-		        argv[0], usage, strlen(usage) ? " " : "");
-		return __LINE__;
-	}
-	auto dataset = TRY_PTR(nullptr, zfs_open(libz, argv[optind], ZFS_TYPE_FILESYSTEM));
-	quickscope_wrapper dataset_deleter{[&] { zfs_close(dataset); }};
-
-	{
-		char encryption_root[MAXNAMELEN];
-		boolean_t dataset_is_root;
-		TRY("get encryption root", zfs_crypto_get_encryption_root(dataset, &dataset_is_root, encryption_root));
-
-		if(!dataset_is_root && !strlen(encryption_root)) {
-			fprintf(stderr, "Dataset %s not encrypted?\n", zfs_get_name(dataset));
-			return __LINE__;
-		} else if(!dataset_is_root) {
-			printf("Using dataset %s's encryption root %s instead.\n", zfs_get_name(dataset), encryption_root);
-			// TODO: disallow maybe? or require force option?
-			zfs_close(dataset);
-			dataset = TRY_PTR(nullptr, zfs_open(libz, encryption_root, ZFS_TYPE_FILESYSTEM));
-		}
-	}
-
-
-	return main(dataset);
+	return main(libz);
+}
+
+template <class G, class M>
+int do_main(int argc, char ** argv, const char * getoptions, const char * usage, G && getoptfn, M && main) {
+	return do_bare_main(argc, argv, getoptions, usage, getoptfn, [&](auto libz) {
+		if(optind >= argc) {
+			fprintf(stderr,
+			        "No dataset to act on?\n"
+			        "Usage: %s [-hV] %s%s<dataset>\n",
+			        argv[0], usage, strlen(usage) ? " " : "");
+			return __LINE__;
+		}
+		auto dataset = TRY_PTR(nullptr, zfs_open(libz, argv[optind], ZFS_TYPE_FILESYSTEM | ZFS_TYPE_VOLUME));
+		quickscope_wrapper dataset_deleter{[&] { zfs_close(dataset); }};
+
+		{
+			char encryption_root[MAXNAMELEN];
+			boolean_t dataset_is_root;
+			TRY("get encryption root", zfs_crypto_get_encryption_root(dataset, &dataset_is_root, encryption_root));
+
+			if(!dataset_is_root && !strlen(encryption_root)) {
+				fprintf(stderr, "Dataset %s not encrypted?\n", zfs_get_name(dataset));
+				return __LINE__;
+			} else if(!dataset_is_root) {
+				printf("Using dataset %s's encryption root %s instead.\n", zfs_get_name(dataset), encryption_root);
+				// TODO: disallow maybe? or require force option?
+				zfs_close(dataset);
+				dataset = TRY_PTR(nullptr, zfs_open(libz, encryption_root, ZFS_TYPE_FILESYSTEM | ZFS_TYPE_VOLUME));
+			}
+		}
+
+
+		return main(dataset);
+	});
 }
diff --git a/src/parse.hpp b/src/parse.hpp
index 7eee9e1..64ab701 100644
--- a/src/parse.hpp
+++ b/src/parse.hpp
@@ -11,7 +11,7 @@
 
 template <class T>
 int parse_int(const char * what, T & out) {
-	int base = 0;
+	int base = 10;
 	if(!strncmp(what, "0x", 2) || !strncmp(what, "0X", 2)) {
 		base = 16;
 		what += 2;
diff --git a/src/zfs.hpp b/src/zfs.hpp
index 54d105c..08b7bab 100644
--- a/src/zfs.hpp
+++ b/src/zfs.hpp
@@ -16,6 +16,8 @@
 #define PROPNAME_BACKEND "xyz.nabijaczleweli:tzpfms.backend"
 #define PROPNAME_KEY "xyz.nabijaczleweli:tzpfms.key"
 
+#define MAXDEPTH_UNSET (SIZE_MAX - 1)
+
 
 /// Mimic libzfs error output
 #define REQUIRE_KEY_LOADED(dataset)                                                  \
@@ -76,3 +78,49 @@ int verify_backend(zfs_handle_t * on, const char * this_backend, F && func) {
 
 	return 0;
 }
+
+
+template <class F>
+struct for_all_datasets_iterator_data {
+	F & func;
+	zfs_iter_f iterator;
+	size_t depth;
+};
+
+/// Iterate over datasets like zfs(8) list
+template <class F>
+int for_all_datasets(libzfs_handle_t * libz, char ** datasets, size_t maxdepth, F && func) {
+	auto iterator = [](zfs_handle_t * dataset, void * dat_p) {
+		auto dat = reinterpret_cast<for_all_datasets_iterator_data<F> *>(dat_p);
+		TRY_MAIN(dat->func(dataset));
+
+		if(dat->depth) {
+			for_all_datasets_iterator_data<F> ndat{dat->func, dat->iterator, dat->depth - 1};
+			return zfs_iter_filesystems(dataset, dat->iterator, &ndat);
+		} else
+			return 0;
+	};
+
+	if(!*datasets) {
+		for_all_datasets_iterator_data<F> dat{func, iterator, (maxdepth == MAXDEPTH_UNSET) ? SIZE_MAX : maxdepth};
+		switch(auto err = zfs_iter_root(libz, dat.iterator, &dat)) {
+			case -1:  // zfs_iter_filesystems() only bubbles errors from callback, but zfs_iter_root() might produce new ones and return -1
+				TRY("iterate root datasets", err);
+				__builtin_unreachable();
+			case 0:
+				return 0;
+			default:
+				return err;
+		}
+	} else {
+		for_all_datasets_iterator_data<F> dat{func, iterator, (maxdepth == MAXDEPTH_UNSET) ? 0 : maxdepth};
+		for(; *datasets; ++datasets) {
+			auto dataset = zfs_open(libz, *datasets, ZFS_TYPE_FILESYSTEM | ZFS_TYPE_VOLUME);
+			if(!dataset)
+				continue;  // error printed by libzfs; mirror zfs(8) list behaviour here and continue despite any errors
+
+			TRY_MAIN(dat.iterator(dataset, &dat));
+		}
+		return 0;
+	}
+}