diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 68216a55d59c2460a69a40a2eb6bdaafbb15efa9..19822fd6eb510fdf37f619f92f497c4aebfe74a0 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -47,6 +47,9 @@ pytest:
     - export LANG=C  # Hopefully detect anything relying on locale
     - python3 -m pip install ".[test]" --index-url https://git.xfel.eu/api/v4/groups/501/-/packages/pypi/simple
     - python3 -m pytest --color yes --verbose --cov=cal_tools --cov=xfel_calibrate --cov-report html:htmlcov --cov-report term
+    - config_dir=$(mktemp -d)
+    - git clone "https://${CONFIG_REPO_DEPLOY_TOKEN}@git.xfel.eu/detectors/calibration_configurations.git" "$config_dir"
+    - python3 -m xfel_calibrate.validate_nbs_config --config-dir "$config_dir"
   coverage: '/TOTAL.*? (\d+(?:\.\d+)?\%)$/'
   artifacts:
     expose_as: 'Coverage report'
diff --git a/notebooks/AGIPD/Characterize_AGIPD_Gain_Darks_NBC.ipynb b/notebooks/AGIPD/Characterize_AGIPD_Gain_Darks_NBC.ipynb
index 371720faf4c2559e822358dd4e1d9dd9f008b317..55a2a2f78a61b91913bceaa039b1b37c2e804c9b 100644
--- a/notebooks/AGIPD/Characterize_AGIPD_Gain_Darks_NBC.ipynb
+++ b/notebooks/AGIPD/Characterize_AGIPD_Gain_Darks_NBC.ipynb
@@ -43,7 +43,7 @@
     "sort_runs = True  # Sort the selected dark runs. This flag is added for old data (e.g. 900174 r0011).\n",
     "\n",
     "mem_cells = 0 # number of memory cells used, set to 0 to automatically infer\n",
-    "bias_voltage = 0 # bias voltage, set to 0 to use stored value in slow data.\n",
+    "bias_voltage = 0. # bias voltage, set to 0 to use stored value in slow data.\n",
     "gain_setting = -1  # the gain setting, use -1 to use value stored in slow data.\n",
     "gain_mode = -1  # gain mode, use -1 to use value stored in slow data.\n",
     "integration_time = -1 # integration time, negative values for auto-detection.\n",
diff --git a/notebooks/Jungfrau/Jungfrau_Gain_Correct_and_Verify_NBC.ipynb b/notebooks/Jungfrau/Jungfrau_Gain_Correct_and_Verify_NBC.ipynb
index 2e3b47d3f2f09c971585de7ceb416a3575a2c584..f4c8d91ca93ff4dad003bed49f5d72ae6723d573 100644
--- a/notebooks/Jungfrau/Jungfrau_Gain_Correct_and_Verify_NBC.ipynb
+++ b/notebooks/Jungfrau/Jungfrau_Gain_Correct_and_Verify_NBC.ipynb
@@ -50,12 +50,12 @@
     "replace_wrong_gain_value = 0  # Force gain value into the chosen gain [0, 1, or 2] for pixels specified in `wrong_gain_pixels`. This has no effect if wrong_gain_pixels = [-1]\n",
     "\n",
     "# Parameters for retrieving calibration constants\n",
-    "integration_time = -1  # integration time in us. set to -1 to overwrite by value in file.\n",
+    "integration_time = -1.  # integration time in us. set to -1 to overwrite by value in file.\n",
     "exposure_timeout = -1  # Exposure timeout. set to -1 to overwrite by value in file.\n",
     "gain_setting = -1  # 0 for dynamic gain, 1 for dynamic HG0. set to -1 to overwrite by value in file.\n",
     "gain_mode = -1  # 0 for runs with dynamic gain setting, 1 for fixed gain. Set to -1 to overwrite by value in file.\n",
     "mem_cells = -1  # Set mem_cells to -1 to automatically use the value stored in RAW data.\n",
-    "bias_voltage = -1  # Bias Voltage. Set to -1 to overwrite by value in file.\n",
+    "bias_voltage = -1.  # Bias Voltage. Set to -1 to overwrite by value in file.\n",
     "\n",
     "# Parameters for plotting\n",
     "skip_plots = False  # exit after writing corrected files\n",
diff --git a/notebooks/LPDMini/LPD_Mini_Correct.ipynb b/notebooks/LPDMini/LPD_Mini_Correct.ipynb
index 0898a4fdc23eb08282020092a14dd37ef56cfb1f..7e2b27d78dcc524cfb781816783b8a25e9ba33a1 100644
--- a/notebooks/LPDMini/LPD_Mini_Correct.ipynb
+++ b/notebooks/LPDMini/LPD_Mini_Correct.ipynb
@@ -41,8 +41,8 @@
     "cal_db_root = '/gpfs/exfel/d/cal/caldb_store'\n",
     "\n",
     "# Operating conditions\n",
-    "bias_voltage_0 = -1 # bias voltage for minis 1, 3, 5, 7; Setting -1 will read the value from files\n",
-    "bias_voltage_1 = -1 # bias voltage for minis 2, 4, 6, 8; Setting -1 will read the value from files\n",
+    "bias_voltage_0 = -1. # bias voltage for minis 1, 3, 5, 7; Setting -1 will read the value from files\n",
+    "bias_voltage_1 = -1. # bias voltage for minis 2, 4, 6, 8; Setting -1 will read the value from files\n",
     "capacitor = '5pF'  # Capacitor setting: 5pF or 50pF\n",
     "photon_energy = 9.3  # Photon energy in keV.\n",
     "use_cell_order = 'auto'  # Whether to use memory cell order as a detector condition (auto = used only when memory cells wrap around)\n",
diff --git a/src/xfel_calibrate/validate_nbs_config.py b/src/xfel_calibrate/validate_nbs_config.py
new file mode 100644
index 0000000000000000000000000000000000000000..ff949658ff0b16e9c9a732a4484fd1e5f471cc6c
--- /dev/null
+++ b/src/xfel_calibrate/validate_nbs_config.py
@@ -0,0 +1,188 @@
+import argparse
+import sys
+from pathlib import Path
+
+import nbformat
+import nbparameterise
+import yaml
+
+from .notebooks import notebooks
+
+PKG_DIR = Path(__file__).parent
+ACTIONS = ("CORRECT", "DARK")  # Only these use this config system
+
+
+def params_from_nb(rel_path):
+    abs_path = PKG_DIR / rel_path
+    nb = nbformat.read(abs_path, as_version=4)
+    return nbparameterise.code.extract_parameter_dict(nb)
+
+
+def load_nb_params():
+    res = {}
+    n = 0
+    for detector, det_nbs in notebooks.items():
+        res[detector] = det_params = {}
+        for action in ACTIONS:
+            if action not in det_nbs:
+                continue
+
+            det_params[action] = params_from_nb(det_nbs[action]["notebook"])
+            n += 1
+
+            for dep_nb in det_nbs[action].get("dep_notebooks", []):
+                dep_params = params_from_nb(dep_nb)
+                extras = set(dep_params) - set(det_params[action])
+                if extras:
+                    print(f"Extra parameters in dep notebook for {detector} {action}")
+                    print(" ", dep_nb)
+                    print(" ", sorted(extras))
+
+    print(f"Loaded parameters for {n} correct/dark notebooks\n")
+    return res
+
+n_probs = 0
+
+def config_problem(problem, detail, file, key):
+    global n_probs
+    n_probs += 1
+    print(f"{problem}\n  {detail}\n  file: {file}\n  key: {'/'.join(key)}\n")
+
+
+def bad_list_element_types(value, param: nbparameterise.Parameter):
+    # param.type == list
+    nb_types = {type(e) for e in param.value}
+    if nb_types == {float, int}:
+        nb_types = {float}
+    assert len(nb_types) == 1, f"{param.value}"
+    nb_type = nb_types.pop()
+
+    if not isinstance(value, list):
+        value = [value]  # In the xfel-calibrate CLI, a list of 1 item is not distinct
+
+    if "RANGE ALLOWED" in (param.comment or "").upper():
+        new_value = []
+        # Try to parse ranges
+        for e in value:
+            if not isinstance(e, str):
+                new_value.append(e)
+                continue
+            try:
+                for rcomp in e.split(","):
+                    if "-" in rcomp:
+                        start, end = rcomp.split("-")
+                        new_value += list(range(int(start), int(end)))
+                    else:
+                        new_value += [int(rcomp)]
+            except ValueError:
+                return "Invalid range specified", repr(e)
+        value = new_value
+
+    if isinstance(value, list):
+        if any(not like_type(e, nb_type) for e in value):
+            bad_types = sorted({type(e) for e in value})
+            return ("List element type differs from default in notebook",
+                    f"notebook: {nb_type}, config: {bad_types}")
+    return None, None
+
+
+def like_type(value, param_type):
+    if isinstance(value, param_type):
+        return True
+    if param_type is float and isinstance(value, int):
+        return True
+    return False
+
+
+def check_yaml_file(path: Path, config_dir: Path, nb_params, detectors):
+    rel_path = path.relative_to(config_dir)
+    with path.open('r') as f:
+        cfg = yaml.safe_load(f)
+
+    kid_to_det = {}
+    for kid, d in cfg['data-mapping'].items():
+        det = d['detector-type'].upper()
+        if det in nb_params:
+            kid_to_det[kid] = det
+        else:
+            config_problem(
+                "Unknown detector type in data mapping", det,
+                rel_path, ('data-mapping', kid, 'detector-type')
+            )
+
+    for action in ACTIONS:
+        action = action.lower()
+        for instrument in cfg[action]:
+            inst_cfg = cfg[action][instrument]
+            for kid, det_cfg in inst_cfg.items():
+                try:
+                    det = kid_to_det[kid]
+                except KeyError:
+                    config_problem(
+                        "Detector Karabo ID not in data mapping", kid,
+                        rel_path, (action, instrument)
+                    )
+                    continue
+
+                if detectors and (det not in detectors):
+                    continue
+
+                try:
+                    det_params = nb_params[det][action.upper()]
+                except KeyError:
+                    config_problem(
+                        f"Action not available for detector {det}", action,
+                        rel_path, (action, instrument, kid)
+                    )
+                    continue
+
+                for k, v in (det_cfg or {}).items():
+                    pname = k.replace('-', '_')
+                    try:
+                        param = det_params[pname]
+                    except KeyError:
+                        config_problem(
+                            "Parameter name not found in notebook", pname,
+                            rel_path, (action, instrument, kid)
+                        )
+                        continue
+
+                    if param.type is list:
+                        pblm, detail = bad_list_element_types(v, param)
+                        if pblm:
+                            config_problem(
+                                pblm, detail, rel_path, (action, instrument, kid, k)
+                            )
+                    elif not like_type(v, param.type):
+                        config_problem(
+                            "Type differs from default in notebook",
+                            f"notebook: {param.value!r}, config: {v!r}",
+                            rel_path, (action, instrument, kid, k)
+                        )
+
+
+
+def main(argv=None):
+    ap = argparse.ArgumentParser()
+    ap.add_argument('--config-dir', type=Path, default="~/calibration_config/")
+    ap.add_argument('--detector', choices=notebooks.keys(), action='append')
+    args = ap.parse_args(argv)
+
+    config_dir = args.config_dir.expanduser()
+    if not config_dir.is_dir():
+        sys.exit(f"No such directory: {config_dir}")
+
+    nb_params = load_nb_params()
+
+    files = [config_dir / 'default.yaml'] + list(config_dir.glob('*/*.yaml'))
+    print(f"Checking {len(files)} files in {config_dir}...\n")
+
+    for file in files:
+        check_yaml_file(file, config_dir, nb_params, detectors=args.detector)
+
+    print(f"{n_probs} config issues found")
+    sys.exit(1 if n_probs else 0)
+
+
+if __name__ == "__main__":
+    main()