diff --git a/src/godotiq/tools/bridge/node_ops.py b/src/godotiq/tools/bridge/node_ops.py
index fb58251..7fad5e8 100644
--- a/src/godotiq/tools/bridge/node_ops.py
+++ b/src/godotiq/tools/bridge/node_ops.py
@@ -3,12 +3,16 @@
 from __future__ import annotations
 
 from godotiq.bridge.session import get_bridge
+from godotiq.tools.bridge.spatial_validator import validate_move, validate_scale
 
 MAX_OPERATIONS = 50
 
 
-async def node_ops(operations: list[dict]) -> dict:
-    """Execute batch node operations in the Godot editor.
+async def node_ops(operations: list[dict], scene_data: dict | None = None) -> dict:
+    """Execute batch node operations with optional spatial validation.
+
+    When scene_data is provided and an operation has validate:true,
+    the operation is checked against scene spatial context before execution.
 
     Each operation is a dict with an 'op' key and operation-specific params.
     All operations are grouped as a single undo action in the editor.
@@ -18,9 +22,12 @@ async def node_ops(operations: list[dict]) -> dict:
 
     Args:
         operations: List of operation dicts. Must be non-empty and at most 50 items.
+        scene_data: Optional scene spatial data for validation. When None, validation
+            is skipped even if operations have validate:true.
 
     Returns:
         Dict with 'results' list containing per-operation status dicts.
+        If validation blocks, returns dict with 'status': 'BLOCKED' instead.
 
     Raises:
         ValueError: If operations list is empty or exceeds the 50-operation limit.
@@ -32,5 +39,38 @@ async def node_ops(operations: list[dict]) -> dict:
             f"Too many operations ({len(operations)}); limit is {MAX_OPERATIONS} per call."
         )
 
+    # Validation
+    validations: list[dict] = []
+    has_validate = scene_data is not None and any(op.get("validate") for op in operations)
+
+    if has_validate:
+        for op in operations:
+            if not op.get("validate"):
+                continue
+            result = None
+            op_type = op.get("op")
+            if op_type == "move" and "position" in op:
+                result = validate_move(op["position"], op.get("node", ""), scene_data)
+            elif op_type == "scale" and "scale" in op:
+                result = validate_scale(op["scale"], op.get("node", ""), scene_data)
+            elif op_type == "add_child" and "position" in op:
+                result = validate_move(op["position"], op.get("node", op.get("name", "")), scene_data)
+            if result is not None:
+                validations.append(result)
+
+        blocked_count = sum(1 for v in validations if v.get("severity") == "BLOCKED")
+        if blocked_count > 0:
+            return {
+                "status": "BLOCKED",
+                "validation": validations,
+                "message": "One or more operations blocked by spatial validation. No changes made.",
+                "operations_blocked": blocked_count,
+            }
+
     bridge = await get_bridge()
-    return await bridge.request("node_ops", params={"operations": operations})
+    result = await bridge.request("node_ops", params={"operations": operations})
+
+    if validations:
+        result["validation"] = validations
+
+    return result
diff --git a/tests/test_bridge/test_spatial_validator.py b/tests/test_bridge/test_spatial_validator.py
index b972f88..705ea3a 100644
--- a/tests/test_bridge/test_spatial_validator.py
+++ b/tests/test_bridge/test_spatial_validator.py
@@ -1,4 +1,7 @@
-"""Tests for spatial_validator module (Section 01)."""
+"""Tests for spatial_validator module (Section 01) and node_ops integration (Section 02)."""
+
+import pytest
+from unittest.mock import AsyncMock, patch
 
 from godotiq.tools.bridge.spatial_validator import (
     validate_move,
@@ -6,6 +9,7 @@ from godotiq.tools.bridge.spatial_validator import (
     _get_name_prefix,
     _extract_nodes,
 )
+from godotiq.tools.bridge.node_ops import node_ops
 
 
 def _scene_data(nodes):
@@ -176,3 +180,94 @@ class TestValidateScale:
         result = validate_scale([1.0], "Foo", _scene_data([]))
         assert result["severity"] == "BLOCKED"
         assert result["valid"] is False
+
+
+# --- node_ops integration tests (Section 02) ---
+
+
+class TestNodeOpsValidation:
+    """Tests for node_ops integration with spatial validation."""
+
+    @pytest.fixture
+    def mock_bridge(self):
+        bridge = AsyncMock()
+        bridge.request.return_value = {"results": [{"op": "move", "node": "Shelf_01", "status": "ok"}]}
+        return bridge
+
+    @pytest.fixture
+    def wall_scene_data(self):
+        return _scene_data([
+            {"name": "Wall_East", "type": "StaticBody3D", "position": [5.1, 0, 0], "depth": 1},
+        ])
+
+    @pytest.fixture
+    def warning_scene_data(self):
+        return _scene_data([
+            {"name": "Wall_East", "type": "StaticBody3D", "position": [0.3, 0, 0], "depth": 1},
+        ])
+
+    @pytest.fixture
+    def sibling_scene_data(self):
+        return _scene_data([
+            {"name": "Shelf_01", "type": "MeshInstance3D", "position": [0, 0, 0], "scale": [1, 1, 1]},
+            {"name": "Shelf_02", "type": "MeshInstance3D", "position": [1, 0, 0], "scale": [1, 1, 1]},
+            {"name": "Shelf_03", "type": "MeshInstance3D", "position": [2, 0, 0], "scale": [1, 1, 1]},
+        ])
+
+    async def test_no_validate_flag_bypasses_validation(self, mock_bridge):
+        ops = [{"op": "move", "node": "Shelf_01", "position": [5.0, 0, 0]}]
+        with patch("godotiq.tools.bridge.node_ops.get_bridge", return_value=mock_bridge):
+            result = await node_ops(operations=ops, scene_data=_scene_data([
+                {"name": "Wall_East", "type": "StaticBody3D", "position": [5.1, 0, 0], "depth": 1},
+            ]))
+        assert "results" in result
+        assert "validation" not in result
+        mock_bridge.request.assert_called_once()
+
+    async def test_no_scene_data_bypasses_validation(self, mock_bridge):
+        ops = [{"op": "move", "node": "Shelf_01", "position": [5.0, 0, 0], "validate": True}]
+        with patch("godotiq.tools.bridge.node_ops.get_bridge", return_value=mock_bridge):
+            result = await node_ops(operations=ops, scene_data=None)
+        assert "results" in result
+        mock_bridge.request.assert_called_once()
+
+    async def test_blocked_prevents_bridge_call(self, mock_bridge, wall_scene_data):
+        ops = [{"op": "move", "node": "Shelf_01", "position": [5.0, 0, 0], "validate": True}]
+        with patch("godotiq.tools.bridge.node_ops.get_bridge", return_value=mock_bridge):
+            result = await node_ops(operations=ops, scene_data=wall_scene_data)
+        assert result["status"] == "BLOCKED"
+        assert "validation" in result
+        assert result["operations_blocked"] >= 1
+        mock_bridge.request.assert_not_called()
+
+    async def test_warning_allows_bridge_call(self, mock_bridge, warning_scene_data):
+        ops = [{"op": "move", "node": "Shelf_01", "position": [0, 0, 0], "validate": True}]
+        with patch("godotiq.tools.bridge.node_ops.get_bridge", return_value=mock_bridge):
+            result = await node_ops(operations=ops, scene_data=warning_scene_data)
+        assert "results" in result
+        assert "validation" in result
+        mock_bridge.request.assert_called_once()
+
+    async def test_mixed_batch_atomic_blocking(self, mock_bridge, wall_scene_data):
+        ops = [
+            {"op": "move", "node": "Shelf_01", "position": [5.0, 0, 0], "validate": True},
+            {"op": "move", "node": "Shelf_02", "position": [10, 0, 10]},
+        ]
+        with patch("godotiq.tools.bridge.node_ops.get_bridge", return_value=mock_bridge):
+            result = await node_ops(operations=ops, scene_data=wall_scene_data)
+        assert result["status"] == "BLOCKED"
+        mock_bridge.request.assert_not_called()
+
+    async def test_add_child_with_position_triggers_validation(self, mock_bridge, wall_scene_data):
+        ops = [{"op": "add_child", "parent": "Root", "scene": "res://foo.tscn",
+                "position": [5.0, 0, 0], "validate": True}]
+        with patch("godotiq.tools.bridge.node_ops.get_bridge", return_value=mock_bridge):
+            result = await node_ops(operations=ops, scene_data=wall_scene_data)
+        assert result["status"] == "BLOCKED"
+
+    async def test_scale_op_triggers_validate_scale(self, mock_bridge, sibling_scene_data):
+        ops = [{"op": "scale", "node": "Shelf_04", "scale": [0.01, 0.01, 0.01], "validate": True}]
+        with patch("godotiq.tools.bridge.node_ops.get_bridge", return_value=mock_bridge):
+            result = await node_ops(operations=ops, scene_data=sibling_scene_data)
+        assert "validation" in result
+        mock_bridge.request.assert_called_once()
