diff --git a/repos/gems/recipes/pkg/test-fs_tool/README b/repos/gems/recipes/pkg/test-fs_tool/README
new file mode 100644
index 0000000000..15d76d5aef
--- /dev/null
+++ b/repos/gems/recipes/pkg/test-fs_tool/README
@@ -0,0 +1 @@
+Test for the fs_tool component
diff --git a/repos/gems/recipes/pkg/test-fs_tool/archives b/repos/gems/recipes/pkg/test-fs_tool/archives
new file mode 100644
index 0000000000..74aad9032c
--- /dev/null
+++ b/repos/gems/recipes/pkg/test-fs_tool/archives
@@ -0,0 +1,8 @@
+_/src/init
+_/src/report_rom
+_/src/sequence
+_/src/dummy
+_/src/fs_query
+_/src/fs_tool
+_/src/vfs
+_/src/vfs_import
diff --git a/repos/gems/recipes/pkg/test-fs_tool/hash b/repos/gems/recipes/pkg/test-fs_tool/hash
new file mode 100644
index 0000000000..ae19db8844
--- /dev/null
+++ b/repos/gems/recipes/pkg/test-fs_tool/hash
@@ -0,0 +1 @@
+2019-03-12 6800bf66885ebc9a841c44ada90a59d4b7485082
diff --git a/repos/gems/recipes/pkg/test-fs_tool/runtime b/repos/gems/recipes/pkg/test-fs_tool/runtime
new file mode 100644
index 0000000000..0251fc5575
--- /dev/null
+++ b/repos/gems/recipes/pkg/test-fs_tool/runtime
@@ -0,0 +1,123 @@
+
+
+
+
+
+ [init -> report_rom] report 'fs_query -> listing'*
+ [init -> report_rom] <listing>*
+ [init -> report_rom] <dir path="/fs/items">*
+ [init -> report_rom] <file name="1">first</file>*
+ [init -> report_rom] <file name="2">second</file>*
+ [init -> report_rom] </dir>*
+ [init -> report_rom] </listing>*
+ [init -> test -> remove] Warning: file/items cannot be removed because it is a directory*
+ [init -> test -> remove] remove file /items/2*
+ [init -> test -> remove] Warning: file/3 cannot be removed because there is no such file*
+ [init -> test -> remove] remove file /4*
+ [init -> report_rom] report 'fs_query -> listing'*
+ [init -> report_rom] <listing>*
+ [init -> report_rom] <dir path="/fs/items">*
+ [init -> report_rom] <file name="1">first</file>*
+ [init -> report_rom] </dir>*
+ [init -> report_rom] </listing>*
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ first
+ second
+
+ fourth
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/repos/gems/recipes/src/fs_tool/content.mk b/repos/gems/recipes/src/fs_tool/content.mk
new file mode 100644
index 0000000000..bc10675030
--- /dev/null
+++ b/repos/gems/recipes/src/fs_tool/content.mk
@@ -0,0 +1,10 @@
+SRC_DIR := src/app/fs_tool
+
+include $(GENODE_DIR)/repos/base/recipes/src/content.inc
+
+MIRROR_FROM_REP_DIR := include/gems/vfs.h
+
+content: $(MIRROR_FROM_REP_DIR)
+
+$(MIRROR_FROM_REP_DIR):
+ $(mirror_from_rep_dir)
diff --git a/repos/gems/recipes/src/fs_tool/hash b/repos/gems/recipes/src/fs_tool/hash
new file mode 100644
index 0000000000..8f27916312
--- /dev/null
+++ b/repos/gems/recipes/src/fs_tool/hash
@@ -0,0 +1 @@
+2019-03-12 c9d57e9f88f69a73e394f8a415044c1f6979b87c
diff --git a/repos/gems/recipes/src/fs_tool/used_apis b/repos/gems/recipes/src/fs_tool/used_apis
new file mode 100644
index 0000000000..983ea1d731
--- /dev/null
+++ b/repos/gems/recipes/src/fs_tool/used_apis
@@ -0,0 +1,3 @@
+base
+os
+vfs
diff --git a/repos/gems/run/depot_autopilot.run b/repos/gems/run/depot_autopilot.run
index 11c3430773..939cf932ad 100644
--- a/repos/gems/run/depot_autopilot.run
+++ b/repos/gems/run/depot_autopilot.run
@@ -665,6 +665,7 @@ set default_test_pkgs {
test-fs_rom_update
test-fs_rom_update_fs
test-fs_rom_update_ram
+ test-fs_tool
test-gnatio
test-init
test-init_loop
diff --git a/repos/gems/src/app/fs_tool/README b/repos/gems/src/app/fs_tool/README
new file mode 100644
index 0000000000..5949470b8e
--- /dev/null
+++ b/repos/gems/src/app/fs_tool/README
@@ -0,0 +1,13 @@
+The fs_tool component performs a sequence of file operations on a locally
+configured VFS. The file operations are given the configuration as follows:
+
+!
+!
+! ...
+!
+!
+!
+
+The 'exit="yes"' attribute instructs the component to exit after completing
+the sequence. Otherwise, the component keeps responding to configuration
+changes by executing the operations found in the updated configurations.
diff --git a/repos/gems/src/app/fs_tool/main.cc b/repos/gems/src/app/fs_tool/main.cc
new file mode 100644
index 0000000000..093c0b52ae
--- /dev/null
+++ b/repos/gems/src/app/fs_tool/main.cc
@@ -0,0 +1,129 @@
+/*
+ * \brief Tool for performing a sequence of file operations
+ * \author Norman Feske
+ * \date 2019-03-12
+ */
+
+/*
+ * Copyright (C) 2019 Genode Labs GmbH
+ *
+ * This file is part of the Genode OS framework, which is distributed
+ * under the terms of the GNU Affero General Public License version 3.
+ */
+
+/* Genode includes */
+#include
+#include
+#include
+#include
+#include
+
+namespace Fs_tool {
+ using namespace Genode;
+ struct Main;
+}
+
+
+struct Fs_tool::Main
+{
+ Env &_env;
+
+ Heap _heap { _env.ram(), _env.rm() };
+
+ Attached_rom_dataspace _config { _env, "config" };
+
+ Vfs::Global_file_system_factory _fs_factory { _heap };
+
+ struct Vfs_env : Vfs::Env
+ {
+ Main &_main;
+
+ struct Io_response_dummy : Vfs::Io_response_handler {
+ void handle_io_response(Vfs::Vfs_handle::Context*) override { }
+ } _io_dummy { };
+
+ struct Watch_response_dummy: Vfs::Watch_response_handler {
+ void handle_watch_response(Vfs::Vfs_watch_handle::Context*) override { }
+ } _watch_dummy { };
+
+ Vfs_env(Main &main) : _main(main) { }
+
+ Genode::Env &env() override { return _main._env; }
+ Allocator &alloc() override { return _main._heap; }
+ Vfs::File_system &root_dir() override { return _main._root_dir_fs; }
+ Vfs::Io_response_handler &io_handler() override { return _io_dummy; }
+ Vfs::Watch_response_handler &watch_handler() override { return _watch_dummy; }
+
+ } _vfs_env { *this };
+
+ Vfs::Dir_file_system _root_dir_fs {
+ _vfs_env, _config.xml().sub_node("vfs"), _fs_factory };
+
+ Directory _root_dir { _vfs_env };
+
+ Signal_handler _config_handler {
+ _env.ep(), *this, &Main::_handle_config };
+
+ bool _verbose = false;
+
+ typedef Directory::Path Path;
+
+ void _remove_file(Xml_node);
+
+ void _handle_config()
+ {
+ _config.update();
+
+ Xml_node const config = _config.xml();
+
+ _verbose = config.attribute_value("verbose", false);
+
+ _root_dir_fs.apply_config(config.sub_node("vfs"));
+
+ config.for_each_sub_node([&] (Xml_node operation) {
+ if (operation.has_type("remove-file")) {
+ _remove_file(operation);
+ }
+ });
+
+ if (config.attribute_value("exit", false)) {
+ _env.parent().exit(0);
+ sleep_forever();
+ }
+ }
+
+ Main(Env &env) : _env(env)
+ {
+ _config.sigh(_config_handler);
+ _handle_config();
+ }
+};
+
+
+void Fs_tool::Main::_remove_file(Xml_node operation)
+{
+ Path const path = operation.attribute_value("path", Path());
+
+ if (!_root_dir.file_exists(path)) {
+
+ if (_verbose) {
+ if (_root_dir.directory_exists(path))
+ warning("file", path, " cannot be removed because it is a directory");
+ else
+ warning("file", path, " cannot be removed because there is no such file");
+ }
+ return;
+ }
+
+ if (_verbose)
+ log("remove file ", path);
+
+ _root_dir.unlink(path);
+
+ if (_verbose && _root_dir.file_exists(path))
+ warning("failed to remove file ", path);
+}
+
+
+void Component::construct(Genode::Env &env) { static Fs_tool::Main main(env); }
+
diff --git a/repos/gems/src/app/fs_tool/target.mk b/repos/gems/src/app/fs_tool/target.mk
new file mode 100644
index 0000000000..857e8ceeed
--- /dev/null
+++ b/repos/gems/src/app/fs_tool/target.mk
@@ -0,0 +1,3 @@
+TARGET = fs_tool
+SRC_CC = main.cc
+LIBS += base vfs