Skip to content

Remove locked worktrees, including ones whose directory is gone - #322

Merged
frenchie4111 merged 1 commit into
ness-dev:mainfrom
blindpirate:fix/remove-locked-worktree
Oct 11, 2026
Merged

frenchie4111 merged 1 commit into
ness-dev:mainfrom
blindpirate:fix/remove-locked-worktree

Conversation

@blindpirate

Copy link
Copy Markdown
Contributor

Problem

Deleting a worktree from the sidebar failed with:

fatal: cannot remove a locked working tree;
use 'remove -f -f' to override or unlock first

Claude Code's worktree isolation (EnterWorktree / Agent isolation: "worktree") creates worktrees in its tmp scratchpad and runs git worktree lock on them. When that scratchpad gets cleaned up, git is left with a locked entry whose directory is missing. git worktree list --porcelain reports locked but never prunable, and git worktree prune skips locked entries. As a result, Ness showed the entry as a healthy worktree, Delete failed, and Stale/prune recovery couldn't clear it either.

Fix

  • Parsing: parseWorktreeListPorcelain records locked / lockedReason. A locked entry whose directory is missing is also marked prunable, so it gets the Stale badge and PanesFSM doesn't spawn tabs against a missing cwd.
  • Missing dir + locked: removal just works. pruneWorktrees (the Stale recovery action) unlocks missing-directory locked entries before running git worktree prune. removeWorktree passes --force --force when the directory is gone.
  • Present dir + locked: these get friction because someone locked them on purpose. The sidebar delete shows a window.confirm (the same pattern as the dirty-worktree confirm) that includes the lock reason. On confirm the request carries overrideLock, and the deletion FSM unlocks the worktree before teardown and the trash rename. Without the override, the FSM refuses before teardown and gives a clear error. This also fixes a latent bug: the fast path used to mv a locked worktree into the trash, and then git worktree prune skipped the entry and left a ghost behind.
  • Unlocked dirty worktrees keep the same single --force behavior.

Tests

  • Parser unit tests for locked [reason], covering both the present-directory and missing-directory cases.
  • Deletion FSM tests: refuse without an override (before teardown), unlock before trashing with an override, and proceed without an override when the directory is gone.
  • New real-git integration test (worktree-locked.integration.test.ts) covering list, remove, and prune for locked-present and locked-missing entries.

npm run typecheck, npx electron-vite build, and npx vitest run all pass except worktree-watcher.integration.test.ts > fires again on commit in a LINKED worktree. That test also fails locally on the base commit. It doesn't touch this code (fs.watch timing).

I couldn't run the app locally (the node-pty native rebuild fails on this machine's toolchain), so the confirm dialog hasn't been clicked through in a running app.

Opened on behalf of @blindpirate by Claude via Ness.

Claude Code's worktree isolation locks the worktrees it creates in its
scratchpad. Once that scratchpad is cleaned up, git keeps a locked entry
whose directory is missing. git doesn't flag it prunable and
`git worktree prune` skips it, so Ness showed it as a normal worktree and
deleting it failed with "cannot remove a locked working tree".

- Parse `locked [reason]` from `git worktree list --porcelain`. A locked
  entry whose directory is missing is reported as prunable, so it gets
  the Stale badge and no tabs are spawned against it.
- pruneWorktrees unlocks missing-directory locked entries before
  pruning. Locked entries whose directory still exists are left alone.
- removeWorktree passes --force twice when the directory is gone.
- A locked worktree whose directory still exists asks for confirmation
  (and shows the lock reason) before it's force removed. The deletion FSM
  refuses to remove it without that override, and unlocks it before
  trashing so the prune after the rename doesn't skip it.
@frenchie4111
frenchie4111 self-requested a review October 11, 2026 16:56
@frenchie4111
frenchie4111 merged commit 3834ea6 into ness-dev:main Oct 11, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants