diff --git a/docs/Config_Reference.md b/docs/Config_Reference.md
index b36c47a96a42..d72b51b8d0b6 100644
--- a/docs/Config_Reference.md
+++ b/docs/Config_Reference.md
@@ -1484,8 +1484,8 @@ using this feature may place the printer in an invalid state - see the
 ```
 [force_move]
 #enable_force_move: False
-#   Set to true to enable FORCE_MOVE and SET_KINEMATIC_POSITION
-#   extended G-Code commands. The default is false.
+#   Set to true to enable FORCE_MOVE, SET_KINEMATIC_POSITION, and
+#   CLEAR_KINEMATIC_POSITION extended G-Code commands. The default is false.
 ```
 
 ### [pause_resume]
diff --git a/docs/G-Codes.md b/docs/G-Codes.md
index a6d056bf2e56..4d442d8197cc 100644
--- a/docs/G-Codes.md
+++ b/docs/G-Codes.md
@@ -526,6 +526,14 @@ may lead to internal software errors. This command may invalidate
 future boundary checks; issue a G28 afterwards to reset the
 kinematics.
 
+#### CLEAR_KINEMATIC_POSITION
+`CLEAR_KINEMATIC_POSITION [KEEP=<[X][Y][Z]>]`: Force the low-level
+kinematic code to forget the current position of the toolhead. This
+only clears the homing status of the axes, which allows the position
+to be restored with SET_KINEMATIC_POSITION. Use the KEEP parameter to
+avoid clearing the position of given axes. By default, clears all axes.
+Issue a G28 afterwards to reset the kinematics.
+
 ### [gcode]
 
 The gcode module is automatically loaded.
diff --git a/klippy/extras/force_move.py b/klippy/extras/force_move.py
index 3c05843b23bd..3ca5ece0bf32 100644
--- a/klippy/extras/force_move.py
+++ b/klippy/extras/force_move.py
@@ -49,6 +49,9 @@ def __init__(self, config):
             gcode.register_command('SET_KINEMATIC_POSITION',
                                    self.cmd_SET_KINEMATIC_POSITION,
                                    desc=self.cmd_SET_KINEMATIC_POSITION_help)
+            gcode.register_command('CLEAR_KINEMATIC_POSITION',
+                                   self.cmd_CLEAR_KINEMATIC_POSITION,
+                                   desc=self.cmd_CLEAR_KINEMATIC_POSITION_help)
     def register_stepper(self, config, mcu_stepper):
         self.steppers[mcu_stepper.get_name()] = mcu_stepper
     def lookup_stepper(self, name):
@@ -131,6 +134,15 @@ def cmd_SET_KINEMATIC_POSITION(self, gcmd):
         z = gcmd.get_float('Z', curpos[2])
         logging.info("SET_KINEMATIC_POSITION pos=%.3f,%.3f,%.3f", x, y, z)
         toolhead.set_position([x, y, z, curpos[3]], homing_axes=(0, 1, 2))
+    cmd_CLEAR_KINEMATIC_POSITION_help = "Clear the current kinematic position"
+    def cmd_CLEAR_KINEMATIC_POSITION(self, gcmd):
+        keep = gcmd.get('KEEP', '').upper()
+        axes = [0, 1, 2]
+        for pos, axis in enumerate('XYZ'):
+            if axis in keep:
+                axes.remove(pos)
+        toolhead = self.printer.lookup_object('toolhead')
+        toolhead.clear_position(axes)
 
 def load_config(config):
     return ForceMove(config)
diff --git a/klippy/extras/safe_z_home.py b/klippy/extras/safe_z_home.py
index ca9b8eb59ea3..ead787df4b81 100644
--- a/klippy/extras/safe_z_home.py
+++ b/klippy/extras/safe_z_home.py
@@ -40,8 +40,8 @@ def cmd_G28(self, gcmd):
                 toolhead.set_position(pos, homing_axes=[2])
                 toolhead.manual_move([None, None, self.z_hop],
                                      self.z_hop_speed)
-                if hasattr(toolhead.get_kinematics(), "note_z_not_homed"):
-                    toolhead.get_kinematics().note_z_not_homed()
+                if hasattr(toolhead.get_kinematics(), "clear_position"):
+                    toolhead.get_kinematics().clear_position((2,))
             elif pos[2] < self.z_hop:
                 # If the Z axis is homed, and below z_hop, lift it to z_hop
                 toolhead.manual_move([None, None, self.z_hop],
diff --git a/klippy/kinematics/cartesian.py b/klippy/kinematics/cartesian.py
index 0cd7849a1896..52d238c4794c 100644
--- a/klippy/kinematics/cartesian.py
+++ b/klippy/kinematics/cartesian.py
@@ -58,9 +58,11 @@ def set_position(self, newpos, homing_axes):
             rail.set_position(newpos)
             if i in homing_axes:
                 self.limits[i] = rail.get_range()
-    def note_z_not_homed(self):
-        # Helper for Safe Z Home
-        self.limits[2] = (1.0, -1.0)
+    def clear_position(self, axes=None):
+        axes = axes if axes is not None else (0, 1, 2)
+        for i, _ in enumerate(self.limits):
+            if i in axes:
+                self.limits[i] = (1.0, -1.0)
     def _home_axis(self, homing_state, axis, rail):
         # Determine movement
         position_min, position_max = rail.get_range()
@@ -88,7 +90,7 @@ def home(self, homing_state):
             else:
                 self._home_axis(homing_state, axis, self.rails[axis])
     def _motor_off(self, print_time):
-        self.limits = [(1.0, -1.0)] * 3
+        self.clear_position()
     def _check_endstops(self, move):
         end_pos = move.end_pos
         for i in (0, 1, 2):
diff --git a/klippy/kinematics/corexy.py b/klippy/kinematics/corexy.py
index d3b755001916..6378f7c75f62 100644
--- a/klippy/kinematics/corexy.py
+++ b/klippy/kinematics/corexy.py
@@ -43,9 +43,11 @@ def set_position(self, newpos, homing_axes):
             rail.set_position(newpos)
             if i in homing_axes:
                 self.limits[i] = rail.get_range()
-    def note_z_not_homed(self):
-        # Helper for Safe Z Home
-        self.limits[2] = (1.0, -1.0)
+    def clear_position(self, axes=None):
+        axes = axes if axes is not None else (0, 1, 2)
+        for i, _ in enumerate(self.limits):
+            if i in axes:
+                self.limits[i] = (1.0, -1.0)
     def home(self, homing_state):
         # Each axis is homed independently and in order
         for axis in homing_state.get_axes():
@@ -63,7 +65,7 @@ def home(self, homing_state):
             # Perform homing
             homing_state.home_rails([rail], forcepos, homepos)
     def _motor_off(self, print_time):
-        self.limits = [(1.0, -1.0)] * 3
+        self.clear_position()
     def _check_endstops(self, move):
         end_pos = move.end_pos
         for i in (0, 1, 2):
diff --git a/klippy/kinematics/corexz.py b/klippy/kinematics/corexz.py
index 72134c32741b..ef11f1eabf07 100644
--- a/klippy/kinematics/corexz.py
+++ b/klippy/kinematics/corexz.py
@@ -43,9 +43,11 @@ def set_position(self, newpos, homing_axes):
             rail.set_position(newpos)
             if i in homing_axes:
                 self.limits[i] = rail.get_range()
-    def note_z_not_homed(self):
-        # Helper for Safe Z Home
-        self.limits[2] = (1.0, -1.0)
+    def clear_position(self, axes=None):
+        axes = axes if axes is not None else (0, 1, 2)
+        for i, _ in enumerate(self.limits):
+            if i in axes:
+                self.limits[i] = (1.0, -1.0)
     def home(self, homing_state):
         # Each axis is homed independently and in order
         for axis in homing_state.get_axes():
@@ -63,7 +65,7 @@ def home(self, homing_state):
             # Perform homing
             homing_state.home_rails([rail], forcepos, homepos)
     def _motor_off(self, print_time):
-        self.limits = [(1.0, -1.0)] * 3
+        self.clear_position()
     def _check_endstops(self, move):
         end_pos = move.end_pos
         for i in (0, 1, 2):
diff --git a/klippy/kinematics/hybrid_corexy.py b/klippy/kinematics/hybrid_corexy.py
index 62dccc266193..7bedc3ac7ff3 100644
--- a/klippy/kinematics/hybrid_corexy.py
+++ b/klippy/kinematics/hybrid_corexy.py
@@ -78,9 +78,11 @@ def set_position(self, newpos, homing_axes):
             rail.set_position(newpos)
             if i in homing_axes:
                 self.limits[i] = rail.get_range()
-    def note_z_not_homed(self):
-        # Helper for Safe Z Home
-        self.limits[2] = (1.0, -1.0)
+    def clear_position(self, axes=None):
+        axes = axes if axes is not None else (0, 1, 2)
+        for i, _ in enumerate(self.limits):
+            if i in axes:
+                self.limits[i] = (1.0, -1.0)
     def _home_axis(self, homing_state, axis, rail):
         position_min, position_max = rail.get_range()
         hi = rail.get_homing_info()
@@ -104,7 +106,7 @@ def home(self, homing_state):
             else:
                 self._home_axis(homing_state, axis, self.rails[axis])
     def _motor_off(self, print_time):
-        self.limits = [(1.0, -1.0)] * 3
+        self.clear_position()
     def _check_endstops(self, move):
         end_pos = move.end_pos
         for i in (0, 1, 2):
diff --git a/klippy/kinematics/hybrid_corexz.py b/klippy/kinematics/hybrid_corexz.py
index a92a21948ba9..e8a0784fa211 100644
--- a/klippy/kinematics/hybrid_corexz.py
+++ b/klippy/kinematics/hybrid_corexz.py
@@ -78,9 +78,11 @@ def set_position(self, newpos, homing_axes):
             rail.set_position(newpos)
             if i in homing_axes:
                 self.limits[i] = rail.get_range()
-    def note_z_not_homed(self):
-        # Helper for Safe Z Home
-        self.limits[2] = (1.0, -1.0)
+    def clear_position(self, axes=None):
+        axes = axes if axes is not None else (0, 1, 2)
+        for i, _ in enumerate(self.limits):
+            if i in axes:
+                self.limits[i] = (1.0, -1.0)
     def _home_axis(self, homing_state, axis, rail):
         position_min, position_max = rail.get_range()
         hi = rail.get_homing_info()
@@ -104,7 +106,7 @@ def home(self, homing_state):
             else:
                 self._home_axis(homing_state, axis, self.rails[axis])
     def _motor_off(self, print_time):
-        self.limits = [(1.0, -1.0)] * 3
+        self.clear_position()
     def _check_endstops(self, move):
         end_pos = move.end_pos
         for i in (0, 1, 2):
diff --git a/klippy/kinematics/polar.py b/klippy/kinematics/polar.py
index deed26d06009..cac2b8d39a12 100644
--- a/klippy/kinematics/polar.py
+++ b/klippy/kinematics/polar.py
@@ -51,9 +51,13 @@ def set_position(self, newpos, homing_axes):
             self.limit_z = self.rails[1].get_range()
         if 0 in homing_axes and 1 in homing_axes:
             self.limit_xy2 = self.rails[0].get_range()[1]**2
-    def note_z_not_homed(self):
-        # Helper for Safe Z Home
-        self.limit_z = (1.0, -1.0)
+    def clear_position(self, axes=None):
+        axes = axes if axes is not None else (0, 1, 2)
+        if 0 in axes or 1 in axes:
+            # X and Y cannot be cleared separately
+            self.limit_xy2 = -1.
+        if 2 in axes:
+            self.limit_z = (1.0, -1.0)
     def _home_axis(self, homing_state, axis, rail):
         # Determine movement
         position_min, position_max = rail.get_range()
@@ -86,8 +90,7 @@ def home(self, homing_state):
         if home_z:
             self._home_axis(homing_state, 2, self.rails[1])
     def _motor_off(self, print_time):
-        self.limit_z = (1.0, -1.0)
-        self.limit_xy2 = -1.
+        self.clear_position()
     def check_move(self, move):
         end_pos = move.end_pos
         xy2 = end_pos[0]**2 + end_pos[1]**2
diff --git a/klippy/toolhead.py b/klippy/toolhead.py
index d8b9382570b6..4c9365b4edce 100644
--- a/klippy/toolhead.py
+++ b/klippy/toolhead.py
@@ -414,6 +414,10 @@ def set_position(self, newpos, homing_axes=()):
         self.commanded_pos[:] = newpos
         self.kin.set_position(newpos, homing_axes)
         self.printer.send_event("toolhead:set_position")
+    def clear_position(self, axes):
+        self.flush_step_generation()
+        self.kin.clear_position(axes)
+        self.printer.send_event("toolhead:set_position")
     def move(self, newpos, speed):
         move = Move(self, self.commanded_pos, newpos, speed)
         if not move.move_d: