From e48554a3e0d5bec74e619070c3fd3f03cac52716 Mon Sep 17 00:00:00 2001 From: JoaoMarcos44 <87440198+JoaoMarcos44@users.noreply.github.com> Date: Thu, 18 Jun 2026 15:55:50 -0700 Subject: [PATCH] feat(cli): lock hermes worktrees so concurrent processes can't clobber them MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit git worktree lock at creation and unlock before removal. A locked worktree refuses 'git worktree remove' (and prune), so a second hermes process or a stray cleanup can't silently delete an in-use isolated worktree. Fail-soft on both paths — a lock/unlock error never blocks the session or cleanup. Salvaged from #47029 (Issue #46303). Unlock moved to the actual-removal path so a preserved (unpushed-commits) worktree stays locked while in use. --- cli.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/cli.py b/cli.py index 4e4ddb015c0..f6a9393d34a 100644 --- a/cli.py +++ b/cli.py @@ -1340,6 +1340,17 @@ def _setup_worktree(repo_root: str = None) -> Optional[Dict[str, str]]: except Exception as e: logger.debug("Error copying .worktreeinclude entries: %s", e) + # Lock the worktree so other processes (and `git worktree remove`) can see + # it is actively in use. Fail-soft: a lock failure never blocks the session. + try: + subprocess.run( + ["git", "worktree", "lock", "--reason", f"hermes pid={os.getpid()}", str(wt_path)], + capture_output=True, text=True, timeout=10, cwd=repo_root, + ) + logger.debug("Worktree locked: %s (pid=%s)", wt_path, os.getpid()) + except Exception as e: + logger.debug("git worktree lock failed (non-fatal): %s", e) + info = { "path": str(wt_path), "branch": branch_name, @@ -1415,6 +1426,16 @@ def _cleanup_worktree(info: Dict[str, str] = None) -> None: # Remove worktree (even if working tree is dirty — uncommitted # changes without unpushed commits are just artifacts) + # Unlock first so `git worktree remove` isn't blocked by the lock we + # placed at creation time. Fail-soft — never block cleanup. + try: + subprocess.run( + ["git", "worktree", "unlock", wt_path], + capture_output=True, text=True, timeout=10, cwd=repo_root, + ) + except Exception as e: + logger.debug("git worktree unlock failed (non-fatal): %s", e) + try: subprocess.run( ["git", "worktree", "remove", wt_path, "--force"],