How Sandcastle Handles Git Worktree Cleanup with Uncommitted Changes

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 within the hasUncommittedChanges function (lines 72‑77). This utility executes git status --porcelain inside the worktree directory and checks for any output.

// 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 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.

// 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:

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:

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 (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 (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 (line 335), Sandcastle reuses worktrees that contain uncommitted changes rather than creating new ones, allowing you to continue work from previous sandbox sessions.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →