Skip to content

pu clean silently orphans worktree directories when git worktree remove partially fails #161

Description

@2witstudios

Summary

pu clean/pu clean --all can leave the worktree directory (full checkout + node_modules) orphaned on disk — invisible to pu status and git worktree list — while still deregistering the branch and manifest entry as if cleanup succeeded. Found this after 27 stale worktree dirs silently accumulated ~61GB in .pu/worktrees/ on a single project.

Root cause

crates/pu-engine/src/engine/worktree_ops.rs:153, in handle_delete_worktree:

// 2. Remove git worktree directory
let root_path = Path::new(project_root);
let wt_path = paths::worktree_path(root_path, worktree_id);
git::remove_worktree(root_path, &wt_path).await.ok();

git::remove_worktree (crates/pu-engine/src/git.rs:81-85) runs:

run_git(&["worktree", "remove", "--force", &wt_str], repo_root).await?;

If this git worktree remove --force fails partway through — e.g. a lingering process (bun/node) still holding a file handle somewhere under node_modules, or a permission error during the recursive delete — git can still deregister the worktree from its own admin metadata (.git/worktrees/<id>) while the physical directory removal doesn't fully complete. run_git correctly returns an Err in that case (non-zero exit status + stderr), but handle_delete_worktree discards it with .ok() and proceeds anyway:

  1. Deletes the local branch
  2. Deletes the remote branch
  3. Removes the worktree entry from the manifest

From that point on, both git worktree list and pu status report the worktree as fully gone, but the directory is still on disk, fully orphaned, with no tooling able to find or reclaim it. There's no post-removal check (e.g. wt_path.exists()) and no logging of the failure anywhere the user can see it.

Impact

Silent, unbounded disk leak over time as agents are spawned/cleaned repeatedly. On one project this accumulated to ~61GB across 27 orphaned directories before being noticed via manual du.

Suggested fix

In handle_delete_worktree:

  • Check the Result from remove_worktree instead of .ok()-discarding it.
  • If it errors, verify whether wt_path still exists on disk:
    • If gone, proceed as today (transient git warning, no real problem).
    • If still present, either retry with a direct std::fs::remove_dir_all fallback, or keep the manifest entry (marked with an error/stale status) instead of removing it, so pu status/pu clean can still see and retry it later rather than losing all reference to it.
  • Log the failure so it's visible in daemon.log / to the user instead of disappearing entirely.

Happy to send a PR for this if useful.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions