# How Sandcastle Handles Git Worktree Cleanup with Uncommitted Changes

> Sandcastle safely cleans Git worktrees with uncommitted changes. It automatically removes clean worktrees and preserves dirty ones for your review.

- Repository: [Matt Pocock/sandcastle](https://github.com/mattpocock/sandcastle)
- Tags: how-to-guide
- Published: 2026-05-24

---

**Sandcastle detects uncommitted changes using `git status --porcelain` and preserves dirty worktrees for manual inspection while automatically removing clean ones.**

When running sandboxed commands, Sandcastle creates temporary git worktrees to isolate changes from your main repository. After each sandbox execution, the framework must decide whether to delete the temporary worktree or preserve it for debugging. According to the mattpocock/sandcastle source code, this decision relies on detecting uncommitted changes through Git's porcelain status interface.

## Detecting Uncommitted Changes in Worktrees

The detection logic resides in [`src/WorktreeManager.ts`](https://github.com/mattpocock/sandcastle/blob/main/src/WorktreeManager.ts) within the `hasUncommittedChanges` function (lines 72‑77). This utility executes `git status --porcelain` inside the worktree directory and checks for any output.

```typescript
// src/WorktreeManager.ts
export const hasUncommittedChanges = (
  worktreePath: string,
): Effect.Effect<boolean, WorktreeError> =>
  execGit(["status", "--porcelain"], worktreePath).pipe(
    Effect.map((output) => output.trim().length > 0),
  );

```

If the trimmed output contains any characters, the worktree contains unstaged modifications, staged changes, or untracked files, triggering preservation logic.

## Cleanup Decision Logic in SandboxFactory

The orchestration happens in [`src/SandboxFactory.ts`](https://github.com/mattpocock/sandcastle/blob/main/src/SandboxFactory.ts) through the `cleanupWorktree` function (lines 196‑235). This function accepts the worktree path and the sandbox exit status, then uses the detection utility to branch between preservation and removal.

### Preserving Dirty Worktrees

When `hasUncommittedChanges` returns `true`, Sandcastle prints a preservation message via `printWorktreePreservedMessage` and returns the worktree path. This allows users to inspect modifications after the sandbox completes, regardless of whether the sandbox succeeded or failed.

The output includes:

- The reason for preservation
- A `cd` command to enter the worktree
- A `git worktree remove --force` command for manual cleanup

### Removing Clean Worktrees

If no uncommitted changes exist, Sandcastle immediately removes the worktree using `WorktreeManager.remove`, which executes `git worktree remove --force <path>`. For failed exits, it logs a confirmation that the worktree was removed.

### Error Handling and Path Attachment

The cleanup result propagates to error handling via `attachPreservedPath`. This utility decorates `AgentIdleTimeoutError` and `AgentError` instances with the `preservedWorktreePath`, enabling programmatic access to preserved worktrees when timeouts or agent errors occur.

```typescript
// src/SandboxFactory.ts
const attachPreservedPath = <E>(path: string | undefined, e: E | SandboxError) => {
  if (path !== undefined) {
    if (e instanceof AgentIdleTimeoutError) {
      return new AgentIdleTimeoutError({ ...e, preservedWorktreePath: path });
    }
    if (e instanceof AgentError) {
      return new AgentError({ ...e, preservedWorktreePath: path });
    }
  }
  return e;
};

```

## Integration in the Sandbox Lifecycle

The cleanup logic executes in the `release` step of both the bind-mount and isolated provider paths within `WorktreeDockerSandboxFactory`. After the sandbox handle closes, the system calls `cleanupWorktree(worktreeInfo.path, exit)` to determine the worktree's fate.

This consistent behavior across all sandbox providers ensures that temporary worktrees located in `.sandcastle/worktrees/` never disappear unexpectedly when they contain valuable debugging information.

## Working with Preserved Worktrees

When consuming the Sandcastle API, check the `preservedWorktreePath` property in the result to handle leftover worktrees:

```typescript
import { SandboxFactory } from "./SandboxFactory";
import { Effect } from "effect";

Effect.runPromise(
  SandboxFactory.withSandbox((info) =>
    info.sandbox.exec("touch dirty.txt")
  )
).then(({ value, preservedWorktreePath }) => {
  if (preservedWorktreePath) {
    console.log(
      `⚠️ Worktree left behind because it had uncommitted changes: ${preservedWorktreePath}`,
    );
    console.log(
      `→ Inspect with: cd ${preservedWorktreePath}\n` +
      `→ Remove when done with: git worktree remove --force ${preservedWorktreePath}`,
    );
  } else {
    console.log("✅ Worktree cleaned up automatically.");
  }
});

```

You can also manually verify worktree status before removal:

```typescript
import * as WorktreeManager from "./WorktreeManager";

const path = "/abs/path/to/.sandcastle/worktrees/sandcastle-xyz";

WorktreeManager.hasUncommittedChanges(path).then((dirty) => {
  if (dirty) {
    console.log("Worktree is dirty – preserving for user.");
  } else {
    WorktreeManager.remove(path).then(() => {
      console.log("Worktree removed.");
    });
  }
});

```

## Summary

- **Detection**: `WorktreeManager.hasUncommittedChanges` in [`src/WorktreeManager.ts`](https://github.com/mattpocock/sandcastle/blob/main/src/WorktreeManager.ts) (lines 72‑77) runs `git status --porcelain` to identify dirty worktrees
- **Preservation**: Dirty worktrees are kept and users receive commands for manual inspection and cleanup via `printWorktreePreservedMessage`
- **Removal**: Clean worktrees are automatically deleted via `WorktreeManager.remove` using `git worktree remove --force`
- **Error Context**: The `preservedWorktreePath` property attaches to results and specific error types (`AgentIdleTimeoutError`, `AgentError`) for programmatic handling
- **Location**: Worktrees are stored in `.sandcastle/worktrees/` and managed through [`src/SandboxFactory.ts`](https://github.com/mattpocock/sandcastle/blob/main/src/SandboxFactory.ts) (lines 196‑235)

## Frequently Asked Questions

### Where does Sandcastle store temporary worktrees?

Sandcastle creates worktrees inside the `.sandcastle/worktrees/` directory relative to your repository root. Each sandbox run generates a unique subdirectory that gets removed automatically unless it contains uncommitted changes.

### Can I force Sandcastle to remove a worktree even if it has uncommitted changes?

No, Sandcastle intentionally preserves worktrees with uncommitted changes to prevent accidental data loss. You must manually run `git worktree remove --force <path>` after inspecting the files, or delete the directory manually if you no longer need the changes.

### How do I access a preserved worktree after a sandbox timeout?

When an `AgentIdleTimeoutError` occurs, Sandcastle attaches the `preservedWorktreePath` to the error object. Catch this error in your code and access the `preservedWorktreePath` property to locate the worktree for debugging the timeout cause.

### Does Sandcastle reuse existing dirty worktrees?

Yes, according to [`src/WorktreeManager.test.ts`](https://github.com/mattpocock/sandcastle/blob/main/src/WorktreeManager.test.ts) (line 335), Sandcastle reuses worktrees that contain uncommitted changes rather than creating new ones, allowing you to continue work from previous sandbox sessions.