Feature: detect and clean stale-PID worktree lock files at session startup

Resolved 💬 1 comment Opened Apr 21, 2026 by KrisKoernerTR Closed May 28, 2026

Problem

When a Claude Code session ends abnormally (OOM kill, force-quit, power loss, SIGKILL), the
.git/worktrees/<name>/locked file that Claude Code writes to claim an active worktree is
never cleaned up. On the next session start, git worktree list shows the worktree as
locked, and any attempt to prune or reuse it fails:

fatal: worktree '<path>' is locked

This is particularly painful in setups that recycle named worktrees across sessions (e.g.
wt-<feature>-<hash>). Users must manually run git worktree unlock <path> or delete the
lock file to recover.

Proposal

At session startup, before the workspace is entered, Claude Code should:

  1. Enumerate all paths listed in .git/worktrees/*/locked.
  2. For each lock file, read the PID embedded in the lock reason string (Claude Code writes

something like "claude-code PID <pid>").

  1. Check whether that PID is still alive (kill -0 <pid> / os.kill(pid, 0) on macOS/Linux,

OpenProcess on Windows).

  1. If the process is dead, delete the lock file (equivalent to git worktree unlock).
  2. Log a one-line notice: Removed stale worktree lock for <path> (PID <pid> no longer running).

No action should be taken when:

  • The PID is still alive (another Claude Code session is legitimately holding the lock).
  • The lock file has no recognisable PID in it (written by another tool; leave it alone).

Why session-start is the right hook

  • It runs once, before any worktree operation, so the cleanup is invisible to the user.
  • It requires no background daemon or OS-level file-watch.
  • It is idempotent: running it on a clean workspace is a no-op.

Prior art / local workaround

We shipped a local self-heal in session_start.py::unlock_dead_worktrees() plus a
~/bin/claude-clean launchd job that fires 60 s after login. It has been reliable across
dozens of abnormal-exit scenarios on macOS. The core logic is ~30 lines of Python and maps
directly onto the shell equivalent:

for lock_file in .git/worktrees/*/locked; do
  pid=$(grep -oP 'PID \K[0-9]+' "$lock_file" 2>/dev/null) || continue
  kill -0 "$pid" 2>/dev/null && continue   # still alive — skip
  git worktree unlock "${lock_file%/locked}" 2>/dev/null
  echo "Removed stale worktree lock (PID $pid no longer running)"
done

Expected behaviour after fix

| Scenario | Before | After |
|---|---|---|
| Session ends normally | Lock removed on exit (already works) | Same |
| Session killed (SIGKILL / OOM) | Lock persists; next session errors | Lock removed at next session start |
| Another session holds the lock | — | Lock preserved (PID alive check) |
| Lock written by 3rd-party tool (no PID) | — | Lock preserved (no PID → skip) |

Platform

macOS 14 (Sonoma) / macOS 15 (Sequoia); should be equivalent on Linux. Windows would need
OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, pid) in place of kill -0.

View original on GitHub ↗

This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗