diff --git a/apps/fsearch/fsearch.cpp b/apps/fsearch/fsearch.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..238d26ec1b0b5668aa6891239399203e078a0e4a
--- /dev/null
+++ b/apps/fsearch/fsearch.cpp
@@ -0,0 +1,80 @@
+// SPDX-License-Identifier: LGPL-3.0-or-later
+// Copyright © 2021 Florian Fischer
+#include <fcntl.h>
+#include <sys/types.h>
+
+#include <array>
+#include <cstdio>
+#include <cstdlib>
+#include <cstring>
+#include <filesystem>
+#include <iostream>
+#include <string>
+
+#include "Common.hpp"
+#include "CountingPrivateSemaphore.hpp"
+#include "Fiber.hpp"
+#include "Runtime.hpp"
+#include "emper.hpp"
+#include "io.hpp"
+
+namespace fs = std::filesystem;
+
+#define EMPER_RIPGREP_BUFSIZE 4096
+
+const char* needle;
+size_t needle_len;
+
+void search(const std::string& path) {
+	int fd = emper::io::openAndWait(path.c_str(), O_RDONLY);
+	if (fd < 0) {
+		DIE_MSG_ERRNO("open failed");
+	}
+
+	std::array<char, EMPER_RIPGREP_BUFSIZE> buf;
+	size_t bytes_searched = 0;
+
+	ssize_t bytes_read = emper::io::readFileAndWait(fd, buf.data(), buf.size(), bytes_searched);
+	while (bytes_read > 0) {
+		if (memmem(&buf[0], bytes_read, needle, needle_len)) {
+			printf("%s\n", path.c_str());
+			return;
+		}
+
+		bytes_searched += static_cast<size_t>(bytes_read);
+		bytes_read = emper::io::readFileAndWait(fd, buf.data(), buf.size(), -1);
+	}
+
+	if (bytes_read < 0) {
+		DIE_MSG_ERRNO("read failed");
+	}
+}
+
+void walk_dir() {
+	CPS cps;
+	for (const auto& p : fs::recursive_directory_iterator(".")) {
+		if (p.is_regular_file()) {
+			spawn([=] { search(p.path()); }, cps);
+		}
+	}
+
+	cps.wait();
+	exit(EXIT_SUCCESS);
+}
+
+auto main(int argc, char* argv[]) -> int {
+	if (argc < 2) {
+		std::cerr << "Usage: " << argv[0] << " <needle>" << std::endl;
+		return EXIT_FAILURE;
+	}
+
+	needle = argv[1];
+	needle_len = strlen(needle);
+
+	Runtime runtime;
+
+	auto* dirWalker = Fiber::from(walk_dir);
+	runtime.scheduleFromAnywhere(*dirWalker);
+
+	runtime.waitUntilFinished();
+}
diff --git a/apps/fsearch/meson.build b/apps/fsearch/meson.build
new file mode 100644
index 0000000000000000000000000000000000000000..b5b548e034a13e215ea5dc24bd25bcb384ecc27c
--- /dev/null
+++ b/apps/fsearch/meson.build
@@ -0,0 +1,5 @@
+fsearch_exe = executable(
+  'fsearch',
+  'fsearch.cpp',
+  dependencies: emper_dep,
+)
diff --git a/apps/meson.build b/apps/meson.build
index 5075128a54a7118400b8d1d1965615331c7dd69d..015a15df3ff1713f0c0f818ad41c2c33a71f0af0 100644
--- a/apps/meson.build
+++ b/apps/meson.build
@@ -21,3 +21,5 @@ echoclient_exe = executable(
   'EchoClient.cpp',
   dependencies: emper_dep,
 )
+
+subdir('fsearch')
diff --git a/iwyu-mappings.imp b/iwyu-mappings.imp
index 834dff5a2aa9852fe93e269215c318a7f8a0e9ee..9f1e941b74a6e151c1b6dbb4feeecc115167c746 100644
--- a/iwyu-mappings.imp
+++ b/iwyu-mappings.imp
@@ -3,5 +3,7 @@
 	{ include: ["@<gtest/.*>", "private", "<gtest/gtest.h>", "public"] },
 	{ include: ["<urcu/map/urcu-memb.h>", "private", "<urcu.h>", "public"] },
 	{ include: ["<bits/cxxabi_forced.h>", "private", "<ctime>", "public" ] },
+
 	{ symbol: ["__kernel_timespec", "private", "<liburing.h>", "public" ] },
+	{ symbol: ["std::filesystem", "private", "<filesystem>", "public" ] },
 ]