diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index b9fab5d092b766e33361a50ee96fb52c1f921953..f47e76e9a1ad9d672be3ebc57d57d294038a5662 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -6,6 +6,12 @@ order.
 --------------------------
 
 
+Added
+^^^^^
+
+* New :func:`.cluster` wrapper function for the FSL ``cluster`` /
+  ``fsl-cluster`` command (!417).
+
 Changed
 ^^^^^^^
 
diff --git a/fsl/tests/test_wrappers/__init__.py b/fsl/tests/test_wrappers/__init__.py
index e146dbf6aaf91c0091e2eb95206fd45a14093421..4dca534cd4278553f118b3ab4d0c7876735e9976 100644
--- a/fsl/tests/test_wrappers/__init__.py
+++ b/fsl/tests/test_wrappers/__init__.py
@@ -46,3 +46,4 @@ def testenv(*fslexes):
         fslexes = [op.join(fsldir, 'bin', e) for e in fslexes]
         if len(fslexes) == 1: yield fslexes[0]
         else:                 yield fslexes
+testenv.__test__ = False
diff --git a/fsl/tests/test_wrappers/test_cluster.py b/fsl/tests/test_wrappers/test_cluster.py
new file mode 100644
index 0000000000000000000000000000000000000000..9475a026e82b5e192d147cc2d4ba9a4c1defa8af
--- /dev/null
+++ b/fsl/tests/test_wrappers/test_cluster.py
@@ -0,0 +1,69 @@
+#!/usr/bin/env python
+#
+# test_cluster.py -
+#
+# Author: Paul McCarthy <pauldmccarthy@gmail.com>
+#
+
+import contextlib
+import os
+import os.path as op
+import sys
+
+import numpy as np
+
+from fsl.wrappers.cluster import cluster, _cluster
+
+from .  import testenv
+from .. import mockFSLDIR
+
+
+@contextlib.contextmanager
+def reseed(seed):
+    state = np.random.get_state()
+    np.random.seed(seed)
+    try:
+        yield
+    finally:
+        np.random.set_state(state)
+
+
+def test_cluster_wrapper():
+    with testenv('fsl-cluster') as cluster_exe:
+        result   = _cluster('input', 10, mm=True)
+        expected = [cluster_exe, '-i', 'input', '-t', '10', '--mm']
+        expected = ' '.join(expected)
+        assert result.stdout[0] == expected
+
+
+mock_titles  = 'ABCDEFGHIJ'
+mock_cluster = f"""
+#!{sys.executable}
+
+import numpy as np
+
+np.random.seed(12345)
+data = np.random.randint(1, 10, (10, 10))
+
+print('\t'.join('{mock_titles}'))
+for row in data:
+    print('\t'.join([str(val) for val in row]))
+""".strip()
+
+
+def test_cluster():
+    with mockFSLDIR() as fsldir:
+        cluster_exe = op.join(fsldir, 'bin', 'fsl-cluster')
+        with open(cluster_exe, 'wt') as f:
+            f.write(mock_cluster)
+        os.chmod(cluster_exe, 0o755)
+
+        data, titles, result1 = cluster('input', 3.5)
+        result2               = cluster('input', 3.5, load=False)
+
+    with reseed(12345):
+        expected = np.random.randint(1, 10, (10, 10))
+
+    assert np.all(np.isclose(data, expected))
+    assert ''.join(titles) == mock_titles
+    assert result1.stdout == result2.stdout
diff --git a/fsl/wrappers/__init__.py b/fsl/wrappers/__init__.py
index 1f9b0a9ae9d7187d180035de77fb37e63c82a409..3cfb92533333193f19abd08586023b0a66487bfa 100755
--- a/fsl/wrappers/__init__.py
+++ b/fsl/wrappers/__init__.py
@@ -103,6 +103,7 @@ from fsl.wrappers.wrapperutils       import (LOAD,
                                              fslwrapper,
                                              funcwrapper)
 from fsl.wrappers                    import (tbss,)
+from fsl.wrappers.cluster            import (cluster,)
 from fsl.wrappers.bet                import (bet,
                                              robustfov)
 from fsl.wrappers.eddy               import (eddy,
diff --git a/fsl/wrappers/cluster.py b/fsl/wrappers/cluster.py
new file mode 100644
index 0000000000000000000000000000000000000000..6c63e382096c32125c4cd605c1243b814dfa47b7
--- /dev/null
+++ b/fsl/wrappers/cluster.py
@@ -0,0 +1,65 @@
+#!/usr/bin/env python
+#
+# cluster.py - Wrapper for the FSL cluster command.
+#
+# Author: Paul McCarthy <pauldmccarthy@gmail.com>
+#
+"""This module contains the :func:`cluster` function, which calls
+the FSL ``cluster`` / ``fsl-cluster``command.
+"""
+
+
+import io
+import numpy as np
+
+from . import wrapperutils as wutils
+
+
+def cluster(infile, thres, load=True, **kwargs):
+    """Wrapper function for the FSL ``cluster`/``fsl-cluster``) command.
+
+    If ``load is True`` (the default) a tuple is returned, containing:
+      - A numpy array containing the cluster table
+      - A list of column titles
+      - A dictionary containing references to the standard output as an
+        attribute called ``stdout``, and to the output images if any,
+        e.g. ``--oindex``.
+
+    If ``load is False``, only the dictionary is returned.
+    """
+
+    result = _cluster(infile, thres, **kwargs)
+
+    if load:
+        header, data = result.stdout[0].split('\n', 1)
+        titles       = header.split('\t')
+        data         = np.loadtxt(io.StringIO(data), delimiter='\t')
+
+        return data, titles, result
+    else:
+        return result
+
+
+@wutils.fileOrImage('infile', 'othresh', 'olmaxim', 'osize',
+                    'omax', 'omean', 'opvals', 'cope', 'warpvol',)
+@wutils.fslwrapper
+def _cluster(infile, thresh, **kwargs):
+    """Actual wrapper function for the FSL ``cluster``/``fsl-cluster`` command.
+    """
+
+    valmap = {
+        'fractional'     : wutils.SHOW_IF_TRUE,
+        'mm'             : wutils.SHOW_IF_TRUE,
+        'min'            : wutils.SHOW_IF_TRUE,
+        'no_table'       : wutils.SHOW_IF_TRUE,
+        'minclustersize' : wutils.SHOW_IF_TRUE,
+        'scalarname'     : wutils.SHOW_IF_TRUE,
+        'verbose'        : wutils.SHOW_IF_TRUE,
+        'voxthresh'      : wutils.SHOW_IF_TRUE,
+        'voxuncthresh'   : wutils.SHOW_IF_TRUE,
+    }
+
+    cmd  = ['fsl-cluster', '-i', infile, '-t', str(thresh)]
+    cmd += wutils.applyArgStyle('--=', valmap=valmap, **kwargs)
+
+    return cmd