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
cdcommand to enter the worktree - A
git worktree remove --forcecommand 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.hasUncommittedChangesinsrc/WorktreeManager.ts(lines 72‑77) runsgit status --porcelainto 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.removeusinggit worktree remove --force - Error Context: The
preservedWorktreePathproperty attaches to results and specific error types (AgentIdleTimeoutError,AgentError) for programmatic handling - Location: Worktrees are stored in
.sandcastle/worktrees/and managed throughsrc/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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →