@@ -760,6 +760,18 @@ def _rescue_staging_dir(self, extension_id: str) -> Path:
760760 digest = hashlib .sha256 (extension_id .encode ("utf-8" )).hexdigest ()[:16 ]
761761 return self .extensions_dir / f".rescue-staging-{ digest } "
762762
763+ @staticmethod
764+ def _has_keep_config_marker (directory : Path ) -> bool :
765+ """Return True when *directory* contains a valid ``.keep-config`` marker.
766+
767+ The marker is a regular (non-symlink) file written by
768+ ``remove(..., keep_config=True)`` to record explicit provenance. Its
769+ content is intentionally empty — only presence matters, not content.
770+ The symlink guard prevents a crafted symlink from fooling the check.
771+ """
772+ marker = directory / ".keep-config"
773+ return marker .is_file () and not marker .is_symlink ()
774+
763775 @staticmethod
764776 def _collect_manifest_command_names (manifest : ExtensionManifest ) -> Dict [str , str ]:
765777 """Collect command and alias names declared by a manifest.
@@ -1635,8 +1647,7 @@ def _recognized_config_names(
16351647 elif (
16361648 dest_dir .exists ()
16371649 and not self .registry .is_installed (manifest .id )
1638- and (dest_dir / ".keep-config" ).is_file ()
1639- and not (dest_dir / ".keep-config" ).is_symlink ()
1650+ and self ._has_keep_config_marker (dest_dir )
16401651 ):
16411652 for cfg_file in (
16421653 list (dest_dir .glob ("*-config.yml" ))
@@ -2024,6 +2035,7 @@ def remove(self, extension_id: str, keep_config: bool = False) -> bool:
20242035 # distinguish this --keep-config leftover from a directory left
20252036 # by a partially-failed install (which must not have its
20262037 # packaged default configs treated as user-preserved data).
2038+ # Content is intentionally empty — only presence matters.
20272039 (extension_dir / ".keep-config" ).write_text ("" )
20282040 else :
20292041 # Backup config files before deleting
0 commit comments