diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 646f21034f2c724db9eb8514efdac5526585a9cd..cee34ab87f8abc68ffb1d6bb02690b5e5ddab02d 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -2,6 +2,17 @@ This document contains the ``fslpy`` release history in reverse chronological
 order.
 
 
+3.16.0 (Under development)
+--------------------------
+
+
+Added
+^^^^^
+
+* New `silent` option to the :func:`.run` function = passing ``silent=True`` is
+  equivalent to passing ``log={'tee':False}`` (!428).
+
+
 3.15.3 (Thursday 16th November 2023)
 ------------------------------------
 
diff --git a/fsl/tests/test_run.py b/fsl/tests/test_run.py
index ca7f69108103693da7bc0ae5f6affb196fd2794a..28122faaef6880fede9b2f9319f7dc5136031579 100644
--- a/fsl/tests/test_run.py
+++ b/fsl/tests/test_run.py
@@ -20,7 +20,6 @@ import pytest
 import fsl.utils.tempdir                  as tempdir
 from   fsl.utils.platform import platform as fslplatform
 import fsl.utils.run                      as run
-import fsl.utils.fslsub                   as fslsub
 
 from . import make_random_image, mockFSLDIR, CaptureStdout, touch
 
@@ -138,6 +137,13 @@ def test_run_tee():
         assert stdout         == expstdout
         assert capture.stdout == ''
 
+
+        # disable forwarding via silent=True
+        with capture.reset():
+            stdout = run.run('./script.sh 1 2 3', silent=True)
+        assert stdout         == expstdout
+        assert capture.stdout == ''
+
         with capture.reset():
             stdout, stderr = run.run('./script.sh 1 2 3', stderr=True,
                                      log={'tee' : True})
@@ -289,6 +295,8 @@ def mock_fsl_sub(*cmd, **kwargs):
 
     name = op.basename(name)
 
+    kwargs.pop('log', None)
+
     jid = '12345'
     output = run.run(cmd)
 
@@ -323,7 +331,7 @@ def test_run_submit():
 
         jid = run.run('fsltest', submit=True)
         assert jid == '12345'
-        stdout, stderr = fslsub.output(jid)
+        stdout, stderr = run.job_output(jid)
         assert stdout == 'test_script running\n'
         assert stderr == ''
 
@@ -331,7 +339,7 @@ def test_run_submit():
         kwargs = {'name' : 'abcde', 'ram' : '4GB'}
         jid = run.run('fsltest', submit=kwargs)
         assert jid == '12345'
-        stdout, stderr = fslsub.output(jid)
+        stdout, stderr = run.job_output(jid)
         experr = '\n'.join(['{}: {}'.format(k, kwargs[k])
                             for k in sorted(kwargs.keys())]) + '\n'
         assert stdout == 'test_script running\n'
@@ -341,7 +349,7 @@ def test_run_submit():
         kwargs = {'name' : 'abcde', 'ram' : '4GB'}
         jid = run.run('fsltest', submit=True, **kwargs)
         assert jid == '12345'
-        stdout, stderr = fslsub.output(jid)
+        stdout, stderr = run.job_output(jid)
         experr = '\n'.join(['{}: {}'.format(k, kwargs[k])
                             for k in sorted(kwargs.keys())]) + '\n'
         assert stdout == 'test_script running\n'
@@ -482,7 +490,7 @@ def test_func_to_cmd():
         for tmp_dir in (None, '.'):
             for clean in ('never', 'on_success', 'always'):
                 for verbose in (False, True):
-                    cmd = fslsub.func_to_cmd(_good_func, clean=clean, tmp_dir=tmp_dir, verbose=verbose)
+                    cmd = run.func_to_cmd(_good_func, clean=clean, tmp_dir=tmp_dir, verbose=verbose)
                     fn = cmd.split()[-1]
                     assert op.exists(fn)
                     stdout, stderr, exitcode = run.run(cmd, exitcode=True, stdout=True, stderr=True,
@@ -497,7 +505,7 @@ def test_func_to_cmd():
                     else:
                         assert stdout.strip() == 'hello'
 
-                cmd = fslsub.func_to_cmd(_bad_func, clean=clean, tmp_dir=tmp_dir)
+                cmd = run.func_to_cmd(_bad_func, clean=clean, tmp_dir=tmp_dir)
                 fn = cmd.split()[-1]
                 assert op.exists(fn)
                 stdout, stderr, exitcode = run.run(cmd, exitcode=True, stdout=True, stderr=True,
diff --git a/fsl/utils/run.py b/fsl/utils/run.py
index 99187527d7e19772e7dbeac65173fa3c49ab6c08..04d382ff7f194b72b803ada418be02771931b34d 100644
--- a/fsl/utils/run.py
+++ b/fsl/utils/run.py
@@ -188,6 +188,9 @@ def run(*args, **kwargs):
                      - cmd:    Optional file-like or callable to which
                                the command itself is logged.
 
+    :arg silent:   Suppress standard output/error. Equivalent to passing
+                   ``log={'tee' : False}``. Ignored if `log` is also passed.
+
     All other keyword arguments are passed through to the ``subprocess.Popen``
     object (via :func:`_realrun`), unless ``submit=True``, in which case they
     are passed through to the :func:`.fsl_sub` function.
@@ -204,10 +207,11 @@ def run(*args, **kwargs):
     submit         = kwargs.pop('submit',   {})
     cmdonly        = kwargs.pop('cmdonly',  False)
     logg           = kwargs.pop('log',      None)
+    silent         = kwargs.pop('silent',   False)
     args           = prepareArgs(args)
 
     if logg is None:
-        logg = {}
+        logg = {'tee' : not silent}
 
     tee       = logg.get('tee',    True)
     logStdout = logg.get('stdout', None)
@@ -237,7 +241,7 @@ def run(*args, **kwargs):
     # but harmless, as we've popped the "submit" arg above.
     if submit is not None:
         from fsl.wrappers import fsl_sub  # pylint: disable=import-outside-toplevel  # noqa: E501
-        return fsl_sub(*args, **submit, **kwargs)[0].strip()
+        return fsl_sub(*args, log=logg, **submit, **kwargs)[0].strip()
 
     # Run directly - delegate to _realrun
     stdout, stderr, exitcode = _realrun(
@@ -633,10 +637,10 @@ def hold(job_ids, hold_filename=None, timeout=10):
     submit = {
         'jobhold'  : _flatten_job_ids(job_ids),
         'jobtime'  : 1,
-        'name'     : '.hold'
+        'name'     : '.hold',
     }
 
-    run(f'touch {hold_filename}', submit=submit)
+    run(f'touch {hold_filename}', submit=submit, silent=True)
 
     while not op.exists(hold_filename):
         time.sleep(timeout)
diff --git a/fsl/wrappers/wrapperutils.py b/fsl/wrappers/wrapperutils.py
index 3fc318d2c1fca28ab7c2e67009a54bf8ea43cc04..1a931de2a4fef77cd486fa3bff453c8ff7438914 100644
--- a/fsl/wrappers/wrapperutils.py
+++ b/fsl/wrappers/wrapperutils.py
@@ -193,7 +193,16 @@ def genxwrapper(func, runner, funccmd=False):
         exitcode = kwargs.pop('exitcode', opts['exitcode'])
         submit   = kwargs.pop('submit',   opts['submit'])
         cmdonly  = kwargs.pop('cmdonly',  opts['cmdonly'])
-        logg     = kwargs.pop('log',      opts['log'])
+        silent   = kwargs.pop('silent',   False)
+
+        # If silent=True, we need to explicitly set
+        # log, as the run function will otherwise
+        # ignore silent and preferentially use the
+        # value we pass for log.
+        if silent:
+            logg = kwargs.pop('log', {'tee' : False})
+        else:
+            logg = kwargs.pop('log', opts['log'])
 
         if funccmd:
             cmd = run.func_to_cmd(func, args=args, kwargs=kwargs,