diff --git a/.github/scripts/check-workspace.py b/.github/scripts/check-workspace.py
new file mode 100644
index 0000000000000000000000000000000000000000..fb3b53acb0c564c2880d58c51a86e627d8945e35
--- /dev/null
+++ b/.github/scripts/check-workspace.py
@@ -0,0 +1,162 @@
+#!/usr/bin/env python3
+
+# Ensures that:
+# - all crates are added to the root workspace
+# - local dependencies are resolved via `path`
+#
+# It does not check that the local paths resolve to the correct crate. This is already done by cargo.
+#
+# Must be called with a folder containing a `Cargo.toml` workspace file.
+
+import os
+import sys
+import toml
+import argparse
+
+def parse_args():
+	parser = argparse.ArgumentParser(description='Check Rust workspace integrity.')
+
+	parser.add_argument('workspace_dir', help='The directory to check', metavar='workspace_dir', type=str, nargs=1)
+	parser.add_argument('--exclude', help='Exclude crate paths from the check', metavar='exclude', type=str, nargs='*', default=[])
+	
+	args = parser.parse_args()
+	return (args.workspace_dir[0], args.exclude)
+
+def main(root, exclude):
+	workspace_crates = get_members(root, exclude)
+	all_crates = get_crates(root, exclude)
+	print(f'📦 Found {len(all_crates)} crates in total')
+	
+	check_missing(workspace_crates, all_crates)
+	check_links(all_crates)
+
+# Extract all members from a workspace.
+# Return: list of all workspace paths
+def get_members(workspace_dir, exclude):
+	print(f'🔎 Indexing workspace {os.path.abspath(workspace_dir)}')
+
+	root_manifest_path = os.path.join(workspace_dir, "Cargo.toml")
+	if not os.path.exists(root_manifest_path):
+		print(f'❌ No root manifest found at {root_manifest}')
+		sys.exit(1)
+
+	root_manifest = toml.load(root_manifest_path)
+	if not 'workspace' in root_manifest:
+		print(f'❌ No workspace found in root {root_manifest_path}')
+		sys.exit(1)
+
+	if not 'members' in root_manifest['workspace']:
+		return []
+	
+	members = []
+	for member in root_manifest['workspace']['members']:
+		if member in exclude:
+			print(f'❌ Excluded member should not appear in the workspace {member}')
+			sys.exit(1)
+		members.append(member)
+	
+	return members
+
+# List all members of the workspace.
+# Return: Map name -> (path, manifest)
+def get_crates(workspace_dir, exclude_crates) -> dict:
+	crates = {}
+
+	for root, dirs, files in os.walk(workspace_dir):
+		if "target" in root:
+			continue
+		for file in files:
+			if file != "Cargo.toml":
+				continue
+
+			path = os.path.join(root, file)
+			with open(path, "r") as f:
+				content = f.read()
+				manifest = toml.loads(content)
+			
+			if 'workspace' in manifest:
+				if root != workspace_dir:
+					print("⏩ Excluded recursive workspace at %s" % path)
+				continue
+			
+			# Cut off the root path and the trailing /Cargo.toml.
+			path = path[len(workspace_dir)+1:-11]
+			name = manifest['package']['name']
+			if path in exclude_crates:
+				print("⏩ Excluded crate %s at %s" % (name, path))
+				continue
+			crates[name] = (path, manifest)
+	
+	return crates
+
+# Check that all crates are in the workspace.
+def check_missing(workspace_crates, all_crates):
+	print(f'🔎 Checking for missing crates')
+	if len(workspace_crates) == len(all_crates):
+		print(f'✅ All {len(all_crates)} crates are in the workspace')
+		return
+
+	missing = []
+	# Find out which ones are missing.
+	for name, (path, manifest) in all_crates.items():
+		if not path in workspace_crates:
+			missing.append([name, path, manifest])
+	missing.sort()
+
+	for name, path, _manifest in missing:
+		print("❌ %s in %s" % (name, path))
+	print(f'😱 {len(all_crates) - len(workspace_crates)} crates are missing from the workspace')
+	sys.exit(1)
+
+# Check that all local dependencies are good.
+def check_links(all_crates):
+	print(f'🔎 Checking for broken dependency links')
+	links = []
+	broken = []
+
+	for name, (path, manifest) in all_crates.items():
+		def check_deps(deps):
+			for dep in deps:
+				# Could be renamed:
+				dep_name = dep
+				if 'package' in deps[dep]:
+					dep_name = deps[dep]['package']
+				if dep_name in all_crates:
+					links.append((name, dep_name))
+
+					if not 'path' in deps[dep]:
+						broken.append((name, dep_name, "crate must be linked via `path`"))
+						return
+		
+		def check_crate(deps):
+			to_checks = ['dependencies', 'dev-dependencies', 'build-dependencies']
+
+			for to_check in to_checks:
+				if to_check in deps:
+					check_deps(deps[to_check])
+		
+		# There could possibly target dependant deps:
+		if 'target' in manifest:
+			# Target dependant deps can only have one level of nesting:
+			for _, target in manifest['target'].items():
+				check_crate(target)
+		
+		check_crate(manifest)
+
+		
+
+	links.sort()
+	broken.sort()
+
+	if len(broken) > 0:
+		for (l, r, reason) in broken:
+			print(f'❌ {l} -> {r} ({reason})')
+
+		print("💥 %d out of %d links are broken" % (len(broken), len(links)))
+		sys.exit(1)
+	else:
+		print("✅ All %d internal dependency links are correct" % len(links))
+
+if __name__ == "__main__":
+	args = parse_args()
+	main(args[0], args[1])
diff --git a/.github/workflows/check-workspace.yml b/.github/workflows/check-workspace.yml
new file mode 100644
index 0000000000000000000000000000000000000000..3dd812d7d9b3743062553b700adba9d6abd93c50
--- /dev/null
+++ b/.github/workflows/check-workspace.yml
@@ -0,0 +1,23 @@
+name: Check workspace
+
+on:
+  pull_request:
+    paths:
+      - "*.toml"
+  merge_group:
+
+jobs:
+  check-workspace:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.0 (22. Sep 2023)
+
+      - name: install python deps
+        run: pip3 install toml
+
+      - name: check integrity
+        run: >
+            python3 .github/scripts/check-workspace.py .
+            --exclude
+            "substrate/frame/contracts/fixtures/build" 
+            "substrate/frame/contracts/fixtures/contracts/common"
diff --git a/Cargo.toml b/Cargo.toml
index 38cae6f640c3739efad0594103f9bc7e9d984112..3afccc24992f40bb50432b0a78dcbc28f9dafa0d 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -105,6 +105,7 @@ members = [
 	"cumulus/parachains/runtimes/assets/test-utils",
 	"cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo",
 	"cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend",
+	"cumulus/parachains/runtimes/bridge-hubs/common",
 	"cumulus/parachains/runtimes/bridge-hubs/test-utils",
 	"cumulus/parachains/runtimes/collectives/collectives-westend",
 	"cumulus/parachains/runtimes/contracts/contracts-rococo",