# Pi Web Worktree Creation and Dirty Worktree Handling: A Complete Technical Guide

> Learn how Pi Web manages Git worktree creation and handles dirty worktrees. Explore API error detection and force removal options for a smooth workflow.

- Repository: [Alex Yang/pi-web](https://github.com/agegr/pi-web)
- Tags: how-to-guide
- Published: 2026-08-10

---

**Pi Web manages Git worktrees by treating each checkout as a separate session while grouping them under a common project root, with explicit handling for dirty worktrees through API-level error detection and user-controlled force removal.**

Pi Web's architecture enables developers to work on multiple branches simultaneously without losing session history. This guide examines how the agegr/pi-web repository implements worktree lifecycle management, from creation through safe removal of repositories with uncommitted changes.

## How Pi Web Resolves Worktree Projects

The foundation of Pi Web's worktree management sits in [`lib/worktree.ts`](https://github.com/agegr/pi-web/blob/main/lib/worktree.ts), where `resolveProject()` (line 85) determines the relationship between a session's current working directory and its underlying Git structure.

### Project Resolution Properties

When `resolveProject()` analyzes a directory, it returns four critical properties:

| Property | Purpose |
|----------|---------|
| `projectRoot` | Top-level directory containing the shared `.git` directory |
| `branch` | Current Git branch, or `null` for detached HEAD |
| `isWorktree` | `true` only for linked worktrees (not the main repository) |
| `isTopLevel` | `true` when cwd equals worktree root; subdirectories become separate projects |

The function executes `git rev-parse --git-common-dir … --show-toplevel` through the `git()` helper (lines 52-60), then normalizes paths using `toNativePath` and `realPathOrSelf`.

Performance is protected by a 60-second TTL cache on `globalThis.__piProjectCache`, preventing repeated Git invocations for the same directory.

## Creating New Worktrees in Pi Web

The `addWorktree(cwd, branch)` function (lines 93-102) implements a six-step creation process:

1. **Sanitize branch name** — `sanitizeBranchForDir()` converts branch names to safe directory names
2. **Locate repository root** — `getRepoRoot()` finds the main Git directory
3. **Create target directory** — `mkdirSync(..., {recursive:true})` establishes `<repoRoot>-worktrees/<sanitized-branch>`
4. **Execute Git worktree add** — either `git worktree add -- <path> <branch>` for existing branches, or `git worktree add -b <branch> -- <path>` to create from `HEAD`
5. **Register file access** — `allowFileRoot(worktreePath)` enables Pi Web's file API to browse the new location
6. **Invalidate cache** — clears the project cache to reflect the updated layout

```typescript
// Client-side worktree creation
await fetch('/api/worktrees', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ cwd: '/home/user/project', branch: 'feature/awesome' })
});
// Response: { path: '/home/user/project-worktrees/feature-awesome', branch: 'feature/awesome' }

```

## Listing and Managing Existing Worktrees

`listWorktrees(cwd)` (lines 44-78) parses `git worktree list --porcelain` into structured objects with `path`, `branch`, and `isMain` properties. The function filters out prunable or missing worktrees, ensuring the UI only displays usable entries.

## Removing Worktrees and Detecting Dirty States

The `removeWorktree(cwd, worktreePath, force)` function (lines 32-44) implements safety-first deletion:

1. Retrieves current worktree list and locates the target
2. **Blocks main worktree removal** — `target.isMain` triggers an error
3. Executes `git worktree remove [--force] <path>`
4. **Captures dirty errors** — re-throws if Git reports uncommitted changes
5. Clears project cache after successful removal

### API-Level Dirty Worktree Detection

The HTTP route in [`app/api/worktrees/route.ts`](https://github.com/agegr/pi-web/blob/main/app/api/worktrees/route.ts) (lines 95-98) transforms Git errors into machine-readable responses:

```typescript
// Detect dirty worktree errors from git
const dirty = /contains modified or untracked files|is dirty/i.test(message);
return NextResponse.json({ error: message, dirty }, { status: dirty ? 409 : 400 });

```

This design yields two distinct response patterns:

- **409 Conflict** with `dirty: true` — uncommitted or untracked files present
- **400 Bad Request** — other removal failures

The front-end in [`components/SessionSidebar.tsx`](https://github.com/agegr/pi-web/blob/main/components/SessionSidebar.tsx) (lines 792-796) consumes this flag, prompting users before executing `force=true` retries.

### Complete Dirty Worktree Handling Flow

```typescript
async function deleteWorktree(cwd: string, path: string) {
  const resp = await fetch(
    `/api/worktrees?cwd=${encodeURIComponent(cwd)}&path=${encodeURIComponent(path)}`,
    { method: 'DELETE' }
  );
  const data = await resp.json();
  
  if (resp.status === 409 && data.dirty) {
    // Prompt user confirmation, then force removal
    await fetch(
      `/api/worktrees?force=true&cwd=${encodeURIComponent(cwd)}&path=${encodeURIComponent(path)}`,
      { method: 'DELETE' }
    );
  }
}

```

## Why Pi Web Protects Dirty Worktrees

Git's native safety mechanism refuses worktree deletion when local modifications would be lost. Pi Web preserves this protection while adding UX refinement:

- **Explicit user consent** — the `dirty` flag enables informed `force` decisions
- **Data loss prevention** — accidental deletions require deliberate opt-in
- **Abandoned worktree cleanup** — force removal remains available for intentional cleanup

## Summary

- **Project resolution**: `resolveProject()` in [`lib/worktree.ts`](https://github.com/agegr/pi-web/blob/main/lib/worktree.ts) uses Git commands and 60-second caching to map directories to worktree metadata
- **Worktree creation**: `addWorktree()` sanitizes branches, creates directories, registers file access, and handles both existing and new branches
- **Safe removal**: `removeWorktree()` prevents main worktree deletion and surfaces dirty states to callers
- **API translation**: [`app/api/worktrees/route.ts`](https://github.com/agegr/pi-web/blob/main/app/api/worktrees/route.ts) converts Git errors to HTTP 409 responses with structured `dirty` flags
- **User control**: [`SessionSidebar.tsx`](https://github.com/agegr/pi-web/blob/main/SessionSidebar.tsx) interprets dirty flags to prompt for force removal, balancing safety with flexibility

## Frequently Asked Questions

### How does Pi Web prevent accidental deletion of worktrees with uncommitted changes?

Pi Web mirrors Git's native protection by detecting "dirty" errors in [`app/api/worktrees/route.ts`](https://github.com/agegr/pi-web/blob/main/app/api/worktrees/route.ts) using regex matching against Git's error messages. The API returns HTTP 409 with `dirty: true`, requiring the front-end to explicitly request `force=true` after user confirmation. This two-step process ensures intentional deletion of modified worktrees.

### What directory structure does Pi Web use for worktrees?

Pi Web creates worktrees at `<repoRoot>-worktrees/<sanitized-branch>`, where `sanitizeBranchForDir()` converts branch names like `feature/awesome` to filesystem-safe strings. This convention keeps all related checkouts discoverable under a predictable sibling directory of the main repository.

### Can Pi Web remove the main repository worktree?

No. The `removeWorktree()` function explicitly checks `target.isMain` and blocks deletion of the primary worktree. This safeguard prevents destruction of the repository's anchor point, which would orphan all linked worktrees and corrupt the shared `.git` directory structure.

### How does Pi Web optimize worktree resolution performance?

A short-lived cache on `globalThis.__piProjectCache` with 60-second TTL prevents repeated Git command execution for the same directory. The cache is explicitly invalidated after worktree additions and removals to maintain consistency with the actual filesystem state.