{"record":{"id":"52ee2f017652cd33","repo":"datawhalechina/hello-agents","slug":"refusing-to-modify-symlink-rel-path","errorCode":null,"errorMessage":"Refusing to modify symlink: {rel_path}","messagePattern":"Refusing to modify symlink: (.+?)","errorType":"exception","errorClass":"PatchApplyError","httpStatus":null,"severity":"error","filePath":"Co-creation-projects/YYHDBL-HelloCodeAgentCli/code_agent/executors/apply_patch_executor.py","lineNumber":206,"sourceCode":"        确保目标路径在 repo_root 目录下，防止访问仓库外的文件。\n        \n        参数:\n            rel_path: 相对路径字符串\n            \n        返回:\n            Path: 安全的绝对路径对象\n            \n        异常:\n            PatchApplyError: 当路径是绝对路径、包含特殊字符或试图访问仓库外时抛出\n        \"\"\"\n        if rel_path.startswith(\"/\") or rel_path.startswith(\"~\"):\n            raise PatchApplyError(f\"Absolute paths are not allowed: {rel_path}\")\n        target = (self.repo_root / rel_path).resolve()\n        # 检查解析后的路径是否以 repo_root 开头\n        if not str(target).startswith(str(self.repo_root.resolve()) + os.sep) and target != self.repo_root.resolve():\n            raise PatchApplyError(f\"Path escapes repo_root: {rel_path}\")\n        if target.exists() and target.is_symlink():\n            raise PatchApplyError(f\"Refusing to modify symlink: {rel_path}\")\n        return target\n\n    def _enforce_suffix(self, target: Path) -> None:\n        \"\"\"\n        检查目标文件的后缀是否在允许的列表中。\n        防止意外修改二进制文件、配置文件或其他敏感文件。\n        \n        参数:\n            target: 目标文件路径对象\n            \n        异常:\n            PatchApplyError: 当文件后缀不在允许列表中时抛出\n        \"\"\"\n        if target.suffix and target.suffix not in self.allowed_write_suffixes:\n            raise PatchApplyError(f\"Disallowed file suffix for write: {target.suffix}\")\n\n    def _backup_file(self, target: Path, backup_run_dir: Path) -> Path:\n        \"\"\"","sourceCodeStart":188,"sourceCodeEnd":224,"githubUrl":"https://github.com/datawhalechina/hello-agents/blob/606a07d341a47be773fab7f4b71177f53f96b2c3/Co-creation-projects/YYHDBL-HelloCodeAgentCli/code_agent/executors/apply_patch_executor.py#L188-L224","documentation":"Raised by _safe_path when the computed target exists and is a symlink — the executor refuses to modify symlinks so a patch cannot silently rewrite a file outside the repo through a link. In practice this branch is nearly dead code because target comes from Path.resolve(), which already follows symlinks; only dangling symlinks (which resolve() does not chase on non-strict mode... it keeps the path) or unusual platforms can reach it.","triggerScenarios":"rel_path names a dangling symlink inside the repo, or an environment where resolve() leaves the link unresolved; writing through a symlink whose target sits outside repo_root would otherwise defeat the escape check that runs earlier.","commonSituations":"Repos with symlinked config files or node_modules-style links; CI checkouts that materialize some paths as links; attempting to patch a file that is a link into a shared volume.","solutions":["Replace the symlink with a real copy of the file inside the repo, then re-apply the patch.","Point the patch at the link's real target by making that target live under repo_root.","If you maintain the executor, check for symlinks before resolve() (on the joined path) so the guard is actually reachable and meaningful."],"exampleFix":"# before (guard runs after resolve(), mostly unreachable)\ntarget = (self.repo_root / rel_path).resolve()\nif target.exists() and target.is_symlink(): ...\n\n# after (check the link before resolving)\njoined = self.repo_root / rel_path\nif joined.is_symlink():\n    raise PatchApplyError(f\"Refusing to modify symlink: {rel_path}\")\ntarget = joined.resolve()","handlingStrategy":"validation","validationCode":"joined = repo_root / rel_path\nif joined.is_symlink():\n    real = joined.resolve()\n    raise ValueError(f'{rel_path} is a symlink to {real}; patch the real file instead')","typeGuard":"def is_plain_file_in_repo(rel_path: str, repo_root: Path) -> bool:\n    joined = repo_root / rel_path\n    return not joined.is_symlink() and str(joined.resolve()).startswith(str(repo_root.resolve()) + os.sep)","tryCatchPattern":"try:\n    executor.apply(patch)\nexcept PatchApplyError as e:\n    if 'Refusing to modify symlink' in str(e):\n        # resolve the link target and re-issue the patch against the real path inside the repo\n        ...","preventionTips":["Keep patchable source files as regular files, not links.","Check is_symlink() on the joined path before resolve(), since resolve() hides links.","In CI, run a repo lint that flags symlinks pointing outside the repo."],"tags":["patch","symlink","security","apply-patch"],"backgroundTag":null,"analyzedSha":"606a07d341a47be773fab7f4b71177f53f96b2c3","analyzedAt":"2026-08-14T22:57:27.446Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}