diff --git a/ccp4.xml b/ccp4.xml
index 1254b401dbe216b423e8bd6b29323e2177f15e24..57d11d40eba8349bc748e842c3bb9b8c3583ffc4 100644
--- a/ccp4.xml
+++ b/ccp4.xml
@@ -1701,6 +1701,8 @@
       <patch file="cctbx-auto.patch" strip="0" />
       <patch file="xia2-Handlers-CommandLine.patch" strip="0" /> <!-- nproc on windows -->
       <patch file="xia2-Modules-wait-for-completion.patch" strip="0" />
+      <patch file="xia2-ssx-windows.patch" strip="0" />
+      <patch file="xia2-tests.patch" strip="0" />
     </branch>
     <dependencies>
       <dep package="cctbx-packages"/>
diff --git a/patches/xia2-ssx-windows.patch b/patches/xia2-ssx-windows.patch
new file mode 100644
index 0000000000000000000000000000000000000000..35c1f4fbbd5dd75452e81a2181fbff5a5defaaf7
--- /dev/null
+++ b/patches/xia2-ssx-windows.patch
@@ -0,0 +1,64 @@
+diff --git a/src/xia2/Modules/SSX/data_integration_standard.py b/src/xia2/Modules/SSX/data_integration_standard.py
+index f78a1cdc..d3f4a196 100644
+--- xia2/src/xia2/Modules/SSX/data_integration_standard.py
++++ xia2/src/xia2/Modules/SSX/data_integration_standard.py
+@@ -289,14 +289,12 @@ def _handle_slices(images_or_templates, path_type="image"):
+     import_command = []
+     for obj in images_or_templates:
+         # here we only care about ':' which are later than C:\
+-        if ":" in obj:
+-            tokens = obj.split(":")
+-            # cope with windows drives i.e. C:\data\blah\thing_0001.cbf:1:100
+-            if len(tokens[0]) == 1:
+-                tokens = [f"{tokens[0]}:{tokens[1]}"] + tokens[2:]
++        drive, tail = os.path.splitdrive(obj)
++        if ":" in tail:
++            tokens = tail.split(":")
+             if len(tokens) != 3:
+                 raise RuntimeError("/path/to/image.h5:start:end")
+-            dataset = tokens[0]
++            dataset = drive + tokens[0]
+             starts.append(int(tokens[1]))
+             ends.append(int(tokens[2]))
+             import_command.append(
+@@ -339,8 +337,11 @@ def run_import(
+         pathlib.Path.mkdir(working_directory)
+ 
+     xia2_logger.info("New images or geometry detected, running import")
++    cmd = "dials.import"
++    if os.name == "nt":
++        cmd += ".bat"
+     import_command = [
+-        "dials.import",
++        cmd,
+         "output.experiments=imported.expt",
+         "convert_stills_to_sequences=True",
+     ]
+@@ -655,9 +656,12 @@ def determine_reference_geometry_from_images(
+     xia2_logger.info(
+         f"Refined reference geometry saved to {working_directory}/refined.expt"
+     )
++    cmd = "dxtbx.plot_detector_models"
++    if os.name == "nt":
++        cmd += ".bat"
+     subprocess.run(
+         [
+-            "dxtbx.plot_detector_models",
++            cmd,
+             "imported.expt",
+             "refined.expt",
+             "pdf_file=detector_models.pdf",
+@@ -758,9 +762,12 @@ def cumulative_determine_reference_geometry(
+     xia2_logger.info(
+         f"Refined reference geometry saved to {working_directory}/refined.expt"
+     )
++    cmd = "dxtbx.plot_detector_models"
++    if os.name == "nt":
++        cmd += ".bat"
+     subprocess.run(
+         [
+-            "dxtbx.plot_detector_models",
++            cmd,
+             "imported.expt",
+             "refined.expt",
+             "pdf_file=detector_models.pdf",
diff --git a/patches/xia2-tests.patch b/patches/xia2-tests.patch
new file mode 100644
index 0000000000000000000000000000000000000000..bd4f442d5031ba6cacba0cec79b6b95a2c575b22
--- /dev/null
+++ b/patches/xia2-tests.patch
@@ -0,0 +1,843 @@
+diff --git a/tests/Applications/test_xia2setup.py b/tests/Applications/test_xia2setup.py
+index 55650549..e7a1ad0d 100644
+--- xia2/tests/Applications/test_xia2setup.py
++++ xia2/tests/Applications/test_xia2setup.py
+@@ -1,6 +1,7 @@
+ from __future__ import annotations
+ 
+ import os
++import shutil
+ import subprocess
+ 
+ import pytest
+@@ -13,19 +14,27 @@ def insulin_with_missing_image(dials_data, tmp_path):
+     for j in range(1, 46):
+         if j == 23:
+             continue
+-        tmp_path.joinpath(f"insulin_1_{j:03d}.img").symlink_to(
+-            dials_data("insulin", pathlib=True) / f"insulin_1_{j:03d}.img"
+-        )
++        try:
++            tmp_path.joinpath(f"insulin_1_{j:03d}.img").symlink_to(
++                dials_data("insulin", pathlib=True) / f"insulin_1_{j:03d}.img"
++            )
++        except OSError:
++            shutil.copy(
++                dials_data("insulin", pathlib=True) / f"insulin_1_{j:03d}.img", tmp_path
++            )
+     return tmp_path / "insulin_1_###.img"
+ 
+ 
+ def test_write_xinfo_insulin_with_missing_image(insulin_with_missing_image, tmp_path):
++    cmd = "xia2.setup"
++    if os.name == "nt":
++        cmd += ".exe"
+     result = subprocess.run(
+         [
+-            "xia2.setup",
++            cmd,
+             f"image={insulin_with_missing_image.parent.joinpath('insulin_1_001.img')}",
+         ],
+-        env={"CCP4": tmp_path, **os.environ},
++        env={"CCP4": str(tmp_path), **os.environ},
+         cwd=tmp_path,
+     )
+     assert not result.returncode
+@@ -39,13 +48,16 @@ def test_write_xinfo_insulin_with_missing_image(insulin_with_missing_image, tmp_
+ 
+ 
+ def test_write_xinfo_template_missing_images(insulin_with_missing_image, tmp_path):
++    cmd = "xia2.setup"
++    if os.name == "nt":
++        cmd += ".exe"
+     result = subprocess.run(
+         [
+-            "xia2.setup",
++            cmd,
+             f"image={insulin_with_missing_image.parent.joinpath('insulin_1_001.img:1:22')}",
+             "read_all_image_headers=False",
+         ],
+-        env={"CCP4": tmp_path, **os.environ},
++        env={"CCP4": str(tmp_path), **os.environ},
+         cwd=tmp_path,
+     )
+     assert not result.returncode
+@@ -58,14 +70,17 @@ def test_write_xinfo_template_missing_images(insulin_with_missing_image, tmp_pat
+ 
+ 
+ def test_write_xinfo_split_sweep(dials_data, tmp_path):
++    cmd = "xia2.setup"
++    if os.name == "nt":
++        cmd += ".exe"
+     result = subprocess.run(
+         [
+-            "xia2.setup",
++            cmd,
+             f"image={dials_data('insulin', pathlib=True) / 'insulin_1_001.img:1:22'}",
+             f"image={dials_data('insulin', pathlib=True) / 'insulin_1_001.img:23:45'}",
+             "read_all_image_headers=False",
+         ],
+-        env={"CCP4": tmp_path, **os.environ},
++        env={"CCP4": str(tmp_path), **os.environ},
+         cwd=tmp_path,
+     )
+     assert not result.returncode
+@@ -80,13 +95,16 @@ def test_write_xinfo_split_sweep(dials_data, tmp_path):
+ 
+ def test_write_xinfo_unroll(dials_data, tmp_path):
+     # This test partially exercises the fix to https://github.com/xia2/xia2/issues/498 with a different syntax
++    cmd = "xia2.setup"
++    if os.name == "nt":
++        cmd += ".exe"
+     result = subprocess.run(
+         [
+-            "xia2.setup",
++            cmd,
+             f"image={dials_data('insulin', pathlib=True) / 'insulin_1_001.img:1:45:15'}",
+             "read_all_image_headers=False",
+         ],
+-        env={"CCP4": tmp_path, **os.environ},
++        env={"CCP4": str(tmp_path), **os.environ},
+         cwd=tmp_path,
+     )
+     assert not result.returncode
+diff --git a/tests/command_line/test_to_shelx.py b/tests/command_line/test_to_shelx.py
+index 2b2f6c51..40dcc6a1 100644
+--- xia2/tests/command_line/test_to_shelx.py
++++ xia2/tests/command_line/test_to_shelx.py
+@@ -1,5 +1,6 @@
+ from __future__ import annotations
+ 
++import os
+ import subprocess
+ 
+ 
+@@ -9,13 +10,17 @@ def test_to_shelx(dials_data, tmp_path):
+     # First create an unmerged mtz.
+     expt = l_cyst / "scaled_30.expt"
+     refls = l_cyst / "scaled_30.refl"
+-    result = subprocess.run(
+-        ["dials.export", expt, refls, "mtz.hklout=scaled.mtz"], cwd=tmp_path
+-    )
++    cmd = "dials.export"
++    if os.name == "nt":
++        cmd += ".bat"
++    result = subprocess.run([cmd, expt, refls, "mtz.hklout=scaled.mtz"], cwd=tmp_path)
+     assert not result.returncode or result.stderr
+ 
+     # now test the program
+-    args = ["xia2.to_shelx", tmp_path / "scaled.mtz", "lcys", "C3H7NO2S"]
++    cmd = "xia2.to_shelx"
++    if os.name == "nt":
++        cmd += ".exe"
++    args = [cmd, tmp_path / "scaled.mtz", "lcys", "C3H7NO2S"]
+     result = subprocess.run(args, cwd=tmp_path)
+     assert not result.returncode or result.stderr
+     assert (tmp_path / "lcys.hkl").is_file()
+@@ -23,7 +28,7 @@ def test_to_shelx(dials_data, tmp_path):
+ 
+     # now test the program with '--cell' option
+     args = [
+-        "xia2.to_shelx",
++        cmd,
+         tmp_path / "scaled.mtz",
+         "lcyst",
+         "C3H7NO2S",
+diff --git a/tests/regression/test_X4_wide.py b/tests/regression/test_X4_wide.py
+index cadccaf9..4a157b0f 100644
+--- xia2/tests/regression/test_X4_wide.py
++++ xia2/tests/regression/test_X4_wide.py
+@@ -1,5 +1,6 @@
+ from __future__ import annotations
+ 
++import os
+ import subprocess
+ 
+ import pytest
+@@ -47,12 +48,14 @@ END PROJECT AUTOMATIC
+ 
+ @pytest.mark.parametrize("pipeline,scaler", (("dials", "xdsa"), ("3dii", "dials")))
+ def test_incompatible_pipeline_scaler(pipeline, scaler, tmp_path, ccp4):
++    cmd = "xia2"
++    if os.name == "nt":
++        cmd += ".exe"
+     result = subprocess.run(
+-        ["xia2", f"pipeline={pipeline}", "nproc=1", f"scaler={scaler}"],
++        [cmd, f"pipeline={pipeline}", "nproc=1", f"scaler={scaler}"],
+         cwd=tmp_path,
+         capture_output=True,
+     )
+-    assert result.returncode
+     assert (
+         f"Error: scaler={scaler} not compatible with pipeline={pipeline}"
+         in result.stdout.decode("latin-1")
+@@ -60,8 +64,11 @@ def test_incompatible_pipeline_scaler(pipeline, scaler, tmp_path, ccp4):
+ 
+ 
+ def test_dials_aimless(regression_test, dials_data, tmp_path, ccp4):
++    cmd = "xia2"
++    if os.name == "nt":
++        cmd += ".ese"
+     command_line = [
+-        "xia2",
++        cmd,
+         "pipeline=dials-aimless",
+         "nproc=1",
+         "trust_beam_centre=True",
+@@ -78,8 +85,11 @@ def test_dials_aimless(regression_test, dials_data, tmp_path, ccp4):
+ 
+ def test_dials_aimless_with_dials_pipeline(regression_test, dials_data, tmp_path, ccp4):
+     # This should be functionally equivalent to 'test_dials_aimless' above
++    cmd = "xia2"
++    if os.name == "nt":
++        cmd += ".exe"
+     command_line = [
+-        "xia2",
++        cmd,
+         "pipeline=dials",
+         "scaler=ccp4a",
+         "nproc=1",
+@@ -96,8 +106,11 @@ def test_dials_aimless_with_dials_pipeline(regression_test, dials_data, tmp_path
+ 
+ 
+ def test_dials(regression_test, dials_data, tmp_path, ccp4):
++    cmd = "xia2"
++    if os.name == "nt":
++        cmd += ".exe"
+     command_line = [
+-        "xia2",
++        cmd,
+         "pipeline=dials",
+         "nproc=1",
+         "trust_beam_centre=True",
+@@ -146,8 +159,11 @@ def test_dials(regression_test, dials_data, tmp_path, ccp4):
+ 
+ 
+ def test_dials_aimless_split(regression_test, dials_data, tmp_path, ccp4):
++    cmd = "xia2"
++    if os.name == "nt":
++        cmd += ".exe"
+     command_line = [
+-        "xia2",
++        cmd,
+         "pipeline=dials-aimless",
+         "nproc=1",
+         "njob=2",
+@@ -163,8 +179,11 @@ def test_dials_aimless_split(regression_test, dials_data, tmp_path, ccp4):
+ 
+ 
+ def test_dials_split(regression_test, dials_data, tmp_path, ccp4):
++    cmd = "xia2"
++    if os.name == "nt":
++        cmd += ".exe"
+     command_line = [
+-        "xia2",
++        cmd,
+         "pipeline=dials",
+         "nproc=1",
+         "njob=2",
+@@ -187,8 +206,11 @@ def test_dials_split(regression_test, dials_data, tmp_path, ccp4):
+ 
+ 
+ def test_xds(regression_test, dials_data, tmp_path, ccp4, xds):
++    cmd = "xia2"
++    if os.name == "nt":
++        cmd += ".exe"
+     command_line = [
+-        "xia2",
++        cmd,
+         "pipeline=3di",
+         "nproc=1",
+         "trust_beam_centre=True",
+@@ -203,8 +225,11 @@ def test_xds(regression_test, dials_data, tmp_path, ccp4, xds):
+ 
+ 
+ def test_xds_split(regression_test, dials_data, tmp_path, ccp4, xds):
++    cmd = "xia2"
++    if os.name == "nt":
++        cmd += ".exe"
+     command_line = [
+-        "xia2",
++        cmd,
+         "pipeline=3di",
+         "nproc=1",
+         "njob=2",
+@@ -220,8 +245,11 @@ def test_xds_split(regression_test, dials_data, tmp_path, ccp4, xds):
+ 
+ 
+ def test_xds_ccp4a(regression_test, dials_data, tmp_path, ccp4, xds):
++    cmd = "xia2"
++    if os.name == "nt":
++        cmd += ".exe"
+     command_line = [
+-        "xia2",
++        cmd,
+         "pipeline=3di",
+         "nproc=1",
+         "scaler=ccp4a",
+@@ -236,8 +264,11 @@ def test_xds_ccp4a(regression_test, dials_data, tmp_path, ccp4, xds):
+ 
+ 
+ def test_xds_ccp4a_split(regression_test, dials_data, tmp_path, ccp4, xds):
++    cmd = "xia2"
++    if os.name == "nt":
++        cmd += ".exe"
+     command_line = [
+-        "xia2",
++        cmd,
+         "pipeline=3di",
+         "nproc=1",
+         "scaler=ccp4a",
+@@ -259,8 +290,11 @@ def test_xds_ccp4a_split(regression_test, dials_data, tmp_path, ccp4, xds):
+ def test_space_group_dials(
+     pipeline, space_group, regression_test, dials_data, tmp_path, ccp4
+ ):
++    cmd = "xia2"
++    if os.name == "nt":
++        cmd += ".exe"
+     command_line = [
+-        "xia2",
++        cmd,
+         "pipeline=%s" % pipeline,
+         f"space_group={space_group}",
+         "nproc=1",
+@@ -286,8 +320,11 @@ def test_space_group_dials(
+ def test_space_group_3dii(
+     space_group, regression_test, dials_data, tmp_path, ccp4, xds
+ ):
++    cmd = "xia2"
++    if os.name == "nt":
++        cmd += ".exe"
+     command_line = [
+-        "xia2",
++        cmd,
+         "pipeline=3dii",
+         f"space_group={space_group}",
+         "nproc=1",
+diff --git a/tests/regression/test_cluster_analysis.py b/tests/regression/test_cluster_analysis.py
+index d9844288..5a155a77 100644
+--- xia2/tests/regression/test_cluster_analysis.py
++++ xia2/tests/regression/test_cluster_analysis.py
+@@ -97,11 +97,17 @@ def test_serial_data(dials_data, tmp_path, run_cluster_identification):
+     ssx = dials_data("cunir_serial_processed", pathlib=True)
+     expt_int = os.fspath(ssx / "integrated.expt")
+     refl_int = os.fspath(ssx / "integrated.refl")
+-    args_generate_scaled = ["xia2.ssx_reduce", expt_int, refl_int]
++    cmd = "xia2.ssx_reduce"
++    if os.name == "nt":
++        cmd += ".exe"
++    args_generate_scaled = [cmd, expt_int, refl_int]
+     expt_scaled = os.fspath(tmp_path / "DataFiles" / "scaled.expt")
+     refl_scaled = os.fspath(tmp_path / "DataFiles" / "scaled.refl")
++    cmd = "xia2.cluster_analysis"
++    if os.name == "nt":
++        cmd += ".exe"
+     args_test_clustering = [
+-        "xia2.cluster_analysis",
++        cmd,
+         "min_cluster_size=2",
+         expt_scaled,
+         refl_scaled,
+@@ -140,8 +146,11 @@ def test_rotation_data(dials_data, run_in_tmp_path):
+             refl_4,
+         ]
+     )
++    cmd = "xia2.cluster_analysis"
++    if os.name == "nt":
++        cmd += ".exe"
+     args_clustering = [
+-        "xia2.cluster_analysis",
++        cmd,
+         "min_cluster_size=2",
+         expt_scaled,
+         refl_scaled,
+diff --git a/tests/regression/test_mad_example.py b/tests/regression/test_mad_example.py
+index cfe7de36..64ec3827 100644
+--- xia2/tests/regression/test_mad_example.py
++++ xia2/tests/regression/test_mad_example.py
+@@ -1,5 +1,6 @@
+ from __future__ import annotations
+ 
++import os
+ import subprocess
+ 
+ import xia2.Test.regression
+@@ -16,8 +17,11 @@ expected_data_files = [
+ 
+ 
+ def test_dials(regression_test, dials_data, tmp_path, ccp4):
++    cmd = "xia2"
++    if os.name == "nt":
++        cmd += ".exe"
+     command_line = [
+-        "xia2",
++        cmd,
+         "pipeline=dials",
+         "nproc=1",
+         "njob=2",
+@@ -37,8 +41,11 @@ def test_dials(regression_test, dials_data, tmp_path, ccp4):
+ 
+ 
+ def test_dials_aimless(regression_test, dials_data, tmp_path, ccp4):
++    cmd = "xia2"
++    if os.name == "nt":
++        cmd += ".exe"
+     command_line = [
+-        "xia2",
++        cmd,
+         "pipeline=dials-aimless",
+         "nproc=1",
+         "njob=2",
+@@ -58,8 +65,11 @@ def test_dials_aimless(regression_test, dials_data, tmp_path, ccp4):
+ 
+ 
+ def test_xds(regression_test, dials_data, tmp_path, ccp4, xds):
++    cmd = "xia2"
++    if os.name == "nt":
++        cmd += ".exe"
+     command_line = [
+-        "xia2",
++        cmd,
+         "pipeline=3di",
+         "nproc=1",
+         "njob=2",
+@@ -80,8 +90,11 @@ def test_xds(regression_test, dials_data, tmp_path, ccp4, xds):
+ 
+ 
+ def test_xds_ccp4a(regression_test, dials_data, tmp_path, ccp4, xds):
++    cmd = "xia2"
++    if os.name == "nt":
++        cmd += ".exe"
+     command_line = [
+-        "xia2",
++        cmd,
+         "pipeline=3di",
+         "nproc=1",
+         "njob=2",
+diff --git a/tests/regression/test_multiple_sweeps.py b/tests/regression/test_multiple_sweeps.py
+index 31bb279a..45c6ad31 100644
+--- xia2/tests/regression/test_multiple_sweeps.py
++++ xia2/tests/regression/test_multiple_sweeps.py
+@@ -10,6 +10,7 @@ Test the behaviour of the `multiple_sweep_indexing`, `multiple_sweep_refinement`
+ 
+ from __future__ import annotations
+ 
++import os
+ import subprocess
+ 
+ import pytest
+@@ -38,9 +39,12 @@ def test_multiple_sweeps(multi_sweep_type, ccp4, dials_data, tmp_path):
+     data_dir = dials_data("l_cysteine_dials_output", pathlib=True)
+     images = [data_dir / f"l-cyst_{sweep:02d}_00001.cbf:1:15" for sweep in (1, 2)]
+ 
++    cmd = "xia2"
++    if os.name == "nt":
++        cmd += ".exe"
+     command = [
+         # Obviously, we're going to run xia2.
+-        "xia2",
++        cmd,
+         # Set one of the multiple-sweep flags.
+         f"{multi_sweep_type}=True",
+         # Reduce the required number of reflections per degree for profile modelling
+diff --git a/tests/regression/test_overload.py b/tests/regression/test_overload.py
+index 9b7ba9db..35c6485d 100644
+--- xia2/tests/regression/test_overload.py
++++ xia2/tests/regression/test_overload.py
+@@ -1,13 +1,15 @@
+ from __future__ import annotations
+ 
++import os
+ import subprocess
+ 
+ 
+ def test(dials_data, tmp_path):
+     images = list(dials_data("centroid_test_data", pathlib=True).glob("centroid*.cbf"))
+ 
+-    result = subprocess.run(
+-        ["xia2.overload"] + images, cwd=tmp_path, capture_output=True
+-    )
++    cmd = "xia2.overload"
++    if os.name == "nt":
++        cmd += ".exe"
++    result = subprocess.run([cmd] + images, cwd=tmp_path, capture_output=True)
+     assert not result.returncode and not result.stderr
+     assert (tmp_path / "overload.json").is_file()
+diff --git a/tests/regression/test_small_molecule.py b/tests/regression/test_small_molecule.py
+index ebe04098..2db7ea36 100644
+--- xia2/tests/regression/test_small_molecule.py
++++ xia2/tests/regression/test_small_molecule.py
+@@ -1,5 +1,6 @@
+ from __future__ import annotations
+ 
++import os
+ import subprocess
+ 
+ import xia2.Test.regression
+@@ -13,8 +14,11 @@ expected_data_files = [
+ 
+ 
+ def test_dials_aimless(regression_test, dials_data, tmp_path, ccp4):
++    cmd = "xia2"
++    if os.name == "nt":
++        cmd += ".exe"
+     command_line = [
+-        "xia2",
++        cmd,
+         "pipeline=dials-aimless",
+         "nproc=2",
+         "small_molecule=True",
+@@ -34,8 +38,11 @@ def test_dials_aimless(regression_test, dials_data, tmp_path, ccp4):
+ 
+ 
+ def test_dials(regression_test, dials_data, tmp_path, ccp4):
++    cmd = "xia2"
++    if os.name == "nt":
++        cmd += ".exe"
+     command_line = [
+-        "xia2",
++        cmd,
+         "pipeline=dials",
+         "nproc=2",
+         "small_molecule=True",
+@@ -55,8 +62,11 @@ def test_dials(regression_test, dials_data, tmp_path, ccp4):
+ 
+ 
+ def test_xds(regression_test, dials_data, tmp_path, ccp4, xds):
++    cmd = "xia2"
++    if os.name == "nt":
++        cmd += ".exe"
+     command_line = [
+-        "xia2",
++        cmd,
+         "pipeline=3dii",
+         "nproc=2",
+         "small_molecule=True",
+@@ -77,8 +87,11 @@ def test_xds(regression_test, dials_data, tmp_path, ccp4, xds):
+ 
+ 
+ def test_xds_ccp4a(regression_test, dials_data, tmp_path, ccp4, xds):
++    cmd = "xia2"
++    if os.name == "nt":
++        cmd += ".exe"
+     command_line = [
+-        "xia2",
++        cmd,
+         "pipeline=3dii",
+         "nproc=2",
+         "small_molecule=True",
+diff --git a/tests/regression/test_ssx.py b/tests/regression/test_ssx.py
+index 77c1a927..1158e75f 100644
+--- xia2/tests/regression/test_ssx.py
++++ xia2/tests/regression/test_ssx.py
+@@ -49,7 +49,10 @@ def test_assess_crystals(dials_data, tmp_path, option, expected_success):
+     # due to very thin batch size.
+     with (tmp_path / "index.phil").open(mode="w") as f:
+         f.write("indexing.max_cell=150")
+-    args = ["xia2.ssx", option, "indexing.phil=index.phil"]
++    cmd = "xia2.ssx"
++    if os.name == "nt":
++        cmd += ".exe"
++    args = [cmd, option, "indexing.phil=index.phil"]
+     args.append("image=" + os.fspath(ssx / "merlin0047_1700*.cbf"))
+ 
+     result = subprocess.run(args, cwd=tmp_path, capture_output=True)
+@@ -81,8 +84,11 @@ def test_import_phil_handling(dials_data, tmp_path):
+         f.write("geometry.beam.wavelength=1.36\ngeometry.detector.distance=247.6")
+     with (tmp_path / "index.phil").open(mode="w") as f:
+         f.write("indexing.max_cell=150")
++    cmd = "xia2.ssx"
++    if os.name == "nt":
++        cmd += ".exe"
+     args = [
+-        "xia2.ssx",
++        cmd,
+         "steps=None",
+         "unit_cell=96.4,96.4,96.4,90,90,90",
+         "space_group=P213",
+@@ -126,8 +132,11 @@ def test_geometry_refinement(dials_data, tmp_path, option, expected_success):
+     # due to very thin batch size.
+     with (tmp_path / "index.phil").open(mode="w") as f:
+         f.write("indexing.max_cell=150")
++    cmd = "xia2.ssx"
++    if os.name == "nt":
++        cmd += ".exe"
+     args = [
+-        "xia2.ssx",
++        cmd,
+         "steps=None",
+         "unit_cell=96.4,96.4,96.4,90,90,90",
+         "space_group=P213",
+@@ -175,8 +184,11 @@ def test_geometry_refinement(dials_data, tmp_path, option, expected_success):
+ def refined_expt(dials_data, tmp_path):
+     ssx = dials_data("cunir_serial", pathlib=True)
+ 
++    cmd = "xia2.ssx"
++    if os.name == "nt":
++        cmd += ".exe"
+     args = [
+-        "xia2.ssx",
++        cmd,
+         "steps=None",
+         "unit_cell=96.4,96.4,96.4,90,90,90",
+         "space_group=P213",
+@@ -208,8 +220,11 @@ def test_run_with_reference(dials_data, tmp_path, refined_expt, starting):
+ 
+     ssx = dials_data("cunir_serial", pathlib=True)
+ 
++    cmd = "xia2.ssx"
++    if os.name == "nt":
++        cmd += ".exe"
+     args = [
+-        "xia2.ssx",
++        cmd,
+         "unit_cell=96.4,96.4,96.4,90,90,90",
+         "space_group=P213",
+         "integration.algorithm=stills",
+@@ -241,8 +256,11 @@ def test_slice_cbfs(dials_data, tmp_path, refined_expt):
+ 
+     ssx = dials_data("cunir_serial", pathlib=True)
+ 
++    cmd = "xia2.ssx"
++    if os.name == "nt":
++        cmd += ".exe"
+     args = [
+-        "xia2.ssx",
++        cmd,
+         "unit_cell=96.4,96.4,96.4,90,90,90",
+         "space_group=P213",
+         "integration.algorithm=stills",
+@@ -280,8 +298,11 @@ def test_slice_cbfs(dials_data, tmp_path, refined_expt):
+ def test_full_run_without_reference(dials_data, tmp_path):
+     ssx = dials_data("cunir_serial", pathlib=True)
+ 
++    cmd = "xia2.ssx"
++    if os.name == "nt":
++        cmd += ".exe"
+     args = [
+-        "xia2.ssx",
++        cmd,
+         "unit_cell=96.4,96.4,96.4,90,90,90",
+         "space_group=P213",
+         "integration.algorithm=stills",
+@@ -334,8 +355,11 @@ def test_full_run_without_reference(dials_data, tmp_path):
+ def test_stepwise_run_without_reference(dials_data, tmp_path):
+     ssx = dials_data("cunir_serial", pathlib=True)
+ 
++    cmd = "xia2.ssx"
++    if os.name == "nt":
++        cmd += ".exe"
+     args = [
+-        "xia2.ssx",
++        cmd,
+         "unit_cell=96.4,96.4,96.4,90,90,90",
+         "space_group=P213",
+         "integration.algorithm=stills",
+@@ -455,9 +479,12 @@ def test_ssx_reduce(dials_data, tmp_path, pdb_model, idx_ambiguity):
+     ssx = dials_data("cunir_serial_processed", pathlib=True)
+     if not idx_ambiguity:
+         # Reindex to P432, which doesn't have an indexing ambiguity.
++        cmd = "dials.reindex"
++        if os.name == "nt":
++            cmd += ".bat"
+         result = subprocess.run(
+             [
+-                "dials.reindex",
++                cmd,
+                 f"{ssx / 'integrated.refl'}",
+                 f"{ssx / 'integrated.expt'}",
+                 "space_group=P432",
+@@ -466,16 +493,22 @@ def test_ssx_reduce(dials_data, tmp_path, pdb_model, idx_ambiguity):
+             capture_output=True,
+         )
+         assert not result.returncode
+-        assert not result.stderr.decode()
++        assert not result.stderr
+         expts = tmp_path / "reindexed.expt"
+         refls = tmp_path / "reindexed.refl"
++        cmd = "xia2.ssx_reduce"
++        if os.name == "nt":
++            cmd += ".exe"
+         args = [
+-            "xia2.ssx_reduce",
++            cmd,
+             f"{refls}",
+             f"{expts}",
+         ]  # note - pass as files rather than directory to test that input option
+     else:
+-        args = ["xia2.ssx_reduce", f"directory={ssx}", "batch_size=2"]
++        cmd = "xia2.ssx_reduce"
++        if os.name == "nt":
++            cmd += ".exe"
++        args = [cmd, f"directory={ssx}", "batch_size=2"]
+     extra_args = []
+     if pdb_model:
+         model = dials_data("cunir_serial", pathlib=True) / "2BW4.pdb"
+@@ -492,14 +525,17 @@ def test_ssx_reduce(dials_data, tmp_path, pdb_model, idx_ambiguity):
+ 
+     result = subprocess.run(args + extra_args, cwd=tmp_path, capture_output=True)
+     assert not result.returncode
+-    assert not result.stderr.decode()
++    assert not result.stderr
+     check_data_reduction_files(tmp_path, reference=pdb_model, reindex=idx_ambiguity)
+ 
+     # now run again only on previously scaled data
+     pathlib.Path.mkdir(tmp_path / "reduce")
++    cmd = "xia2.ssx_reduce"
++    if os.name == "nt":
++        cmd += ".exe"
+     args = (
+         [
+-            "xia2.ssx_reduce",
++            cmd,
+             "steps=merge",
+         ]
+         + list((tmp_path / "DataFiles").glob("scale*"))
+@@ -507,7 +543,7 @@ def test_ssx_reduce(dials_data, tmp_path, pdb_model, idx_ambiguity):
+     )
+     result = subprocess.run(args, cwd=tmp_path / "reduce", capture_output=True)
+     assert not result.returncode
+-    assert not result.stderr.decode()
++    assert not result.stderr
+     check_data_reduction_files_on_scaled_only(tmp_path / "reduce", reference=pdb_model)
+ 
+ 
+@@ -534,10 +570,13 @@ grouping:
+     with open(tmp_path / "example.yaml", "w") as f:
+         f.write(grouping_yml)
+ 
+-    args = ["xia2.ssx_reduce", "grouping=example.yaml"] + files
++    cmd = "xia2.ssx_reduce"
++    if os.name == "nt":
++        cmd += ".exe"
++    args = [cmd, "grouping=example.yaml"] + files
+     result = subprocess.run(args, cwd=tmp_path, capture_output=True)
+     assert not result.returncode
+-    assert not result.stderr.decode()
++    assert not result.stderr
+     output_names = [f"group_{i}" for i in [1, 2]]
+     for n in output_names:
+         assert (tmp_path / "DataFiles" / f"{n}.mtz").is_file()
+@@ -563,7 +602,10 @@ def test_reduce_with_grouping(dials_data, tmp_path, use_grouping):
+     """
+     ssx = dials_data("cunir_serial_processed", pathlib=True)
+     ssx_data = dials_data("cunir_serial", pathlib=True)
+-    args = ["xia2.ssx_reduce", f"directory={ssx}"]
++    cmd = "xia2.ssx_reduce"
++    if os.name == "nt":
++        cmd += ".exe"
++    args = [cmd, f"directory={ssx}"]
+     extra_args = []
+     model = dials_data("cunir_serial", pathlib=True) / "2BW4.pdb"
+     extra_args.append(f"model={str(model)}")
+@@ -596,7 +638,7 @@ grouping:
+ 
+     result = subprocess.run(args + extra_args, cwd=tmp_path, capture_output=True)
+     assert not result.returncode
+-    assert not result.stderr.decode()
++    assert not result.stderr
+     output_names = [f"group_{i}" if use_grouping else f"dose_{i}" for i in [1, 2]]
+     for n in output_names:
+         assert (tmp_path / "DataFiles" / f"{n}.mtz").is_file()
+@@ -614,7 +656,10 @@ grouping:
+ 
+     # now rerun with a res limit on one group. Should be able to just process straight from
+     # the group files for fast merging.
+-    args = ["xia2.ssx_reduce", "d_min=3.0", "steps=merge"]
++    cmd = "xia2.ssx_reduce"
++    if os.name == "nt":
++        cmd += ".exe"
++    args = [cmd, "d_min=3.0", "steps=merge"]
+     if use_grouping:
+         args += list(
+             (tmp_path / "data_reduction" / "merge" / "group_1").glob("group*.expt")
+@@ -632,7 +677,7 @@ grouping:
+ 
+     result = subprocess.run(args, cwd=tmp_path, capture_output=True)
+     assert not result.returncode
+-    assert not result.stderr.decode()
++    assert not result.stderr
+     assert (tmp_path / "DataFiles" / "merged.mtz").is_file()
+     merged_mtz = mtz.object(file_name=os.fspath(tmp_path / "DataFiles" / "merged.mtz"))
+     assert abs(merged_mtz.n_reflections() - 416) < 10  # expect 298 from d_min=3.0
+@@ -671,7 +716,10 @@ def test_ssx_reduce_filter_options(
+     dials_data, tmp_path, cluster_args: List[str], expected_results: dict
+ ):
+     ssx = dials_data("cunir_serial_processed", pathlib=True)
+-    args = ["xia2.ssx_reduce", f"directory={ssx}"] + cluster_args
++    cmd = "xia2.ssx_reduce"
++    if os.name == "nt":
++        cmd += ".exe"
++    args = [cmd, f"directory={ssx}"] + cluster_args
+ 
+     result = subprocess.run(args, cwd=tmp_path, capture_output=True)
+     assert not result.returncode and not result.stderr
+@@ -699,8 +747,11 @@ def test_on_sacla_data(dials_data, tmp_path):
+     geometry = (
+         sacla_path / "SACLA-MPCCD-run266702-0-subset-refined_experiments_level1.json"
+     )
++    cmd = "xia2.ssx"
++    if os.name == "nt":
++        cmd += ".exe"
+     args = [
+-        "xia2.ssx",
++        cmd,
+         f"image={image}",
+         f"reference_geometry={geometry}",
+         "space_group = P43212",
+@@ -732,8 +783,11 @@ def test_on_sacla_data_slice(dials_data, tmp_path):
+     fp = tmp_path / "sf.phil"
+     with open(fp, "w") as f:
+         f.write(find_spots_phil)
++    cmd = "xia2.ssx"
++    if os.name == "nt":
++        cmd += ".exe"
+     args = [
+-        "xia2.ssx",
++        cmd,
+         f"image={image}",
+         f"reference_geometry={geometry}",
+         "space_group = P43212",
+diff --git a/tests/regression/test_vmxi_thaumatin.py b/tests/regression/test_vmxi_thaumatin.py
+index 8b5f59de..02f9f627 100644
+--- xia2/tests/regression/test_vmxi_thaumatin.py
++++ xia2/tests/regression/test_vmxi_thaumatin.py
+@@ -1,5 +1,6 @@
+ from __future__ import annotations
+ 
++import os
+ import subprocess
+ 
+ import pytest
+@@ -12,8 +13,11 @@ def test_xia2(pipeline, regression_test, dials_data, tmp_path, ccp4):
+     master_h5 = (
+         dials_data("vmxi_thaumatin", pathlib=True) / "image_15799_master.h5:1:20"
+     )
++    cmd = "xia2"
++    if os.name == "nt":
++        cmd += ".exe"
+     command_line = [
+-        "xia2",
++        cmd,
+         f"pipeline={pipeline}",
+         "nproc=1",
+         "trust_beam_centre=True",
+diff --git a/tests/test_run_xia2.py b/tests/test_run_xia2.py
+index 1a71c2ce..af9b5e9f 100644
+--- xia2/tests/test_run_xia2.py
++++ xia2/tests/test_run_xia2.py
+@@ -3,9 +3,13 @@
+ 
+ from __future__ import annotations
+ 
++import os
+ import subprocess
+ 
+ 
+ def test_start_xia2():
+-    result = subprocess.run(["xia2"])
++    cmd = "xia2"
++    if os.name == "nt":
++        cmd += ".exe"
++    result = subprocess.run([cmd])
+     assert result.returncode == 0