diff --git a/flamenco_worker/commands.py b/flamenco_worker/commands.py
index 14a3156467bd022428da5760ac4a560ade97808c..8f419d1bfb36c3a6116cf5b9f7c37d07ac434cb6 100644
--- a/flamenco_worker/commands.py
+++ b/flamenco_worker/commands.py
@@ -324,6 +324,71 @@ class MoveToFinalCommand(AbstractCommand):
         src.rename(dest)
 
 
+@command_executor('copy_file')
+class CopyFileCommand(AbstractCommand):
+    def validate(self, settings: dict):
+        src, err = self._setting(settings, 'src', True)
+        if err:
+            return err
+        if not src:
+            return 'src may not be empty'
+        dest, err = self._setting(settings, 'dest', True)
+        if err:
+            return err
+        if not dest:
+            return 'dest may not be empty'
+
+    async def execute(self, settings: dict):
+        src = Path(settings['src'])
+        if not src.exists():
+            raise CommandExecutionError('Path %s does not exist, unable to copy' % src)
+
+        dest = Path(settings['dest'])
+        if dest.exists():
+            msg = 'Destination %s exists, going to overwrite it.' % dest
+            self._log.info(msg)
+            await self.worker.register_log('%s: %s', self.command_name, msg)
+
+        self._log.info('Copying %s to %s', src, dest)
+        await self.worker.register_log('%s: Copying %s to %s', self.command_name, src, dest)
+
+        if not dest.parent.exists():
+            await self.worker.register_log('%s: Target directory %s does not exist; creating.',
+                                           self.command_name, dest.parent)
+            dest.parent.mkdir(parents=True)
+
+        import shutil
+        shutil.copy(str(src), str(dest))
+
+
+@command_executor('remove_tree')
+class RemoveTreeCommand(AbstractCommand):
+    def validate(self, settings: dict):
+        path, err = self._setting(settings, 'path', True)
+        if err:
+            return err
+        if not path:
+            return "'path' may not be empty"
+
+    async def execute(self, settings: dict):
+        path = Path(settings['path'])
+        if not path.exists():
+            msg = 'Path %s does not exist, so not removing.' % path
+            self._log.debug(msg)
+            await self.worker.register_log(msg)
+            return
+
+        msg = 'Removing tree rooted at %s' % path
+        self._log.info(msg)
+        await self.worker.register_log(msg)
+
+        if path.is_dir():
+            import shutil
+            shutil.rmtree(str(path))
+        else:
+            path.unlink()
+
+
 @attr.s
 class AbstractSubprocessCommand(AbstractCommand):
     readline_timeout = attr.ib(default=SUBPROC_READLINE_TIMEOUT)
diff --git a/tests/test_commands_copy_file.py b/tests/test_commands_copy_file.py
new file mode 100644
index 0000000000000000000000000000000000000000..78408d30cacb0453440d54e364689ec2e2ccf7f8
--- /dev/null
+++ b/tests/test_commands_copy_file.py
@@ -0,0 +1,109 @@
+from pathlib import Path
+import os
+
+from test_runner import AbstractCommandTest
+
+
+class CopyFileTest(AbstractCommandTest):
+    def setUp(self):
+        super().setUp()
+
+        from flamenco_worker.commands import CopyFileCommand
+        import tempfile
+
+        self.tmpdir = tempfile.TemporaryDirectory()
+        self.tmppath = Path(self.tmpdir.name)
+
+        self.cmd = CopyFileCommand(
+            worker=self.fworker,
+            task_id='12345',
+            command_idx=0,
+        )
+
+    def tearDown(self):
+        super().tearDown()
+        self.tmpdir.cleanup()
+
+    def test_validate_settings(self):
+        self.assertIn('src', self.cmd.validate({'src': 12, 'dest': '/valid/path'}))
+        self.assertIn('src', self.cmd.validate({'src': '', 'dest': '/valid/path'}))
+        self.assertIn('dest', self.cmd.validate({'src': '/valid/path', 'dest': 12}))
+        self.assertIn('dest', self.cmd.validate({'src': '/valid/path', 'dest': ''}))
+        self.assertTrue(self.cmd.validate({}))
+        self.assertFalse(self.cmd.validate({'src': '/some/path', 'dest': '/some/path'}))
+
+    def test_nonexistant_source_and_dest(self):
+        src = self.tmppath / 'nonexisting'
+        dest = self.tmppath / 'dest'
+        task = self.cmd.run({'src': str(src), 'dest': str(dest)})
+        ok = self.loop.run_until_complete(task)
+
+        self.assertFalse(ok)
+        self.assertFalse(src.exists())
+        self.assertFalse(dest.exists())
+
+    def test_existing_source__nonexisting_dest(self):
+        src = self.tmppath / 'existing'
+        src.touch()
+        dest = self.tmppath / 'dest'
+        task = self.cmd.run({'src': str(src), 'dest': str(dest)})
+        ok = self.loop.run_until_complete(task)
+
+        self.assertTrue(ok)
+        self.assertTrue(src.exists())
+        self.assertTrue(dest.exists())
+
+    def test_nonexisting_source__existing_dest(self):
+        src = self.tmppath / 'non-existing'
+
+        dest = self.tmppath / 'dest'
+        with open(str(dest), 'w') as outfile:
+            outfile.write('dest')
+
+        task = self.cmd.run({'src': str(src), 'dest': str(dest)})
+        ok = self.loop.run_until_complete(task)
+
+        self.assertFalse(ok)
+        self.assertFalse(src.exists())
+        self.assertTrue(dest.exists())
+
+        with open(str(dest), 'r') as infile:
+            self.assertEqual('dest', infile.read())
+
+    def test_existing_source_and_dest(self):
+        src = self.tmppath / 'existing'
+        with open(str(src), 'w') as outfile:
+            outfile.write('src')
+
+        dest = self.tmppath / 'dest'
+        with open(str(dest), 'w') as outfile:
+            outfile.write('dest')
+
+        task = self.cmd.run({'src': str(src), 'dest': str(dest)})
+        ok = self.loop.run_until_complete(task)
+
+        self.assertTrue(ok)
+        self.assertTrue(src.exists())
+        self.assertTrue(dest.exists())
+
+        with open(str(src), 'r') as infile:
+            self.assertEqual('src', infile.read())
+
+        with open(str(dest), 'r') as infile:
+            self.assertEqual('src', infile.read())
+
+    def test_dest_in_nonexisting_subdir(self):
+        src = self.tmppath / 'existing'
+        with open(str(src), 'w') as outfile:
+            outfile.write('src')
+
+        dest = self.tmppath / 'nonexisting' / 'subdir' / 'dest'
+        task = self.cmd.run({'src': str(src), 'dest': str(dest)})
+        ok = self.loop.run_until_complete(task)
+
+        self.assertTrue(ok)
+        self.assertTrue(src.exists())
+        self.assertTrue(dest.exists())
+
+        with open(str(dest), 'r') as infile:
+            self.assertEqual('src', infile.read())
diff --git a/tests/test_commands_remove_tree.py b/tests/test_commands_remove_tree.py
new file mode 100644
index 0000000000000000000000000000000000000000..8308612f041a8c56f659b30a4050c3ab84c7a437
--- /dev/null
+++ b/tests/test_commands_remove_tree.py
@@ -0,0 +1,93 @@
+from pathlib import Path
+import os
+
+from test_runner import AbstractCommandTest
+
+
+class RemoveTreeTest(AbstractCommandTest):
+    def setUp(self):
+        super().setUp()
+
+        from flamenco_worker.commands import RemoveTreeCommand
+        import tempfile
+
+        self.tmpdir = tempfile.TemporaryDirectory()
+        self.tmppath = Path(self.tmpdir.name)
+
+        self.cmd = RemoveTreeCommand(
+            worker=self.fworker,
+            task_id='12345',
+            command_idx=0,
+        )
+
+    def tearDown(self):
+        super().tearDown()
+        self.tmpdir.cleanup()
+
+    def test_validate_settings(self):
+        self.assertIn('path', self.cmd.validate({'path': 12}))
+        self.assertIn('path', self.cmd.validate({'path': ''}))
+        self.assertIn('path', self.cmd.validate({}))
+        self.assertFalse(self.cmd.validate({'path': '/some/path'}))
+
+    def test_nonexistant_source(self):
+        path = self.tmppath / 'nonexisting'
+        task = self.cmd.run({'path': str(path)})
+        ok = self.loop.run_until_complete(task)
+
+        self.assertTrue(ok)
+        self.assertFalse(path.exists())
+
+    def test_source_file(self):
+        path = self.tmppath / 'existing'
+        path.touch()
+        task = self.cmd.run({'path': str(path)})
+        ok = self.loop.run_until_complete(task)
+
+        self.assertTrue(ok)
+        self.assertFalse(path.exists())
+
+    def test_soure_dir_with_files(self):
+        path = self.tmppath / 'dir'
+        path.mkdir()
+        (path / 'a.file').touch()
+        (path / 'b.file').touch()
+        (path / 'c.file').touch()
+
+        task = self.cmd.run({'path': str(path)})
+        ok = self.loop.run_until_complete(task)
+
+        self.assertTrue(ok)
+        self.assertFalse(path.exists())
+
+    def test_soure_dir_with_files_and_dirs(self):
+        path = self.tmppath / 'dir'
+        path.mkdir()
+        (path / 'subdir-a' / 'subsub-1').mkdir(parents=True)
+        (path / 'subdir-a' / 'subsub-2').mkdir()
+        (path / 'subdir-a' / 'subsub-3').mkdir()
+        (path / 'subdir-b' / 'subsub-1').mkdir(parents=True)
+        (path / 'subdir-c' / 'subsub-1').mkdir(parents=True)
+        (path / 'subdir-c' / 'subsub-2').mkdir()
+        (path / 'a.file').touch()
+        (path / 'b.file').touch()
+        (path / 'c.file').touch()
+
+        (path / 'subdir-a' / 'subsub-1' / 'a.file').touch()
+        (path / 'subdir-a' / 'subsub-1' / 'b.file').touch()
+        (path / 'subdir-a' / 'subsub-2' / 'a.file').touch()
+        (path / 'subdir-a' / 'subsub-2' / 'b.file').touch()
+        (path / 'subdir-a' / 'subsub-3' / 'a.file').touch()
+        (path / 'subdir-a' / 'subsub-3' / 'b.file').touch()
+        (path / 'subdir-b' / 'subsub-1' / 'a.file').touch()
+        (path / 'subdir-b' / 'subsub-1' / 'b.file').touch()
+        (path / 'subdir-c' / 'subsub-1' / 'a.file').touch()
+        (path / 'subdir-c' / 'subsub-1' / 'b.file').touch()
+        (path / 'subdir-c' / 'subsub-2' / 'a.file').touch()
+        (path / 'subdir-c' / 'subsub-2' / 'b.file').touch()
+
+        task = self.cmd.run({'path': str(path)})
+        ok = self.loop.run_until_complete(task)
+
+        self.assertTrue(ok)
+        self.assertFalse(path.exists())