# How pi-web Integrates with Git Worktrees for Branch-Based Development

> Discover how pi-web seamlessly integrates with Git worktrees, treating each branch as a distinct project for efficient branch-based development. Streamline your workflow today.

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

---

**pi-web treats each Git worktree as a separate project while unifying them in the UI through automatic project resolution and session grouping.**

pi-web, an open-source development environment maintained in the `agegr/pi-web` repository, enables seamless branch-based development by leveraging Git worktrees to isolate branches while maintaining a unified interface. The application automatically detects worktree relationships through Git metadata, groups sessions by underlying repository root regardless of which worktree they originated from, and allows dynamic branch switching via a dedicated UI component.

## Project Resolution via Git Metadata

At the heart of pi-web’s worktree integration sits `resolveProject()` in [`lib/worktree.ts`](https://github.com/agegr/pi-web/blob/main/lib/worktree.ts). Whenever a session’s working directory is inspected, this function executes a targeted Git query to discover the common `.git` directory shared by all worktrees:

```

git rev-parse --git-common-dir

```

If the current working directory resides within a linked worktree, `resolveProject()` marks `isWorktree` as `true`, records the current branch name in the `branch` field, and identifies the main repository root. For directories outside Git repositories, it falls back to the cwd itself. To avoid the overhead of repeated Git spawns, results are cached on `globalThis.__piProjectCache` for 60 seconds.

## Session Grouping by Repository Root

The `listAllSessions()` function in [`lib/session-reader.ts`](https://github.com/agegr/pi-web/blob/main/lib/session-reader.ts) loads every session file and invokes `resolveProject()` once per unique cwd. It attaches the returned `ProjectInfo` to each `SessionInfo` object as `projectRoot` and, when applicable, as `worktreeBranch`. This metadata allows the sidebar component to group sessions that belong to the same underlying repository, regardless of whether they were created in the main working tree or a linked worktree.

## REST API for Worktree Operations

The `/api/worktrees` endpoint in [`app/api/worktrees/route.ts`](https://github.com/agegr/pi-web/blob/main/app/api/worktrees/route.ts) exposes three core operations for branch management:

- **GET** – Returns the repository’s `projectRoot`, boolean flags indicating whether the cwd is a Git repo and whether it is the repository top-level, plus a complete list of existing worktrees via `listWorktrees()`.
- **POST** – Invokes `addWorktree()` to create a new directory under `<repoRoot>-worktrees/<sanitized-branch>` and immediately registers the path with the file-access allow-list.
- **DELETE** – Calls `removeWorktree()` to delete a worktree directory, optionally forcing removal of dirty worktrees with uncommitted changes.

### File-Access Gating for Security

Worktree paths are added to the global allow-list through `allowFileRoot()`—implemented in [`lib/file-access.ts`](https://github.com/agegr/pi-web/blob/main/lib/file-access.ts)—both when created via `addWorktree()` and when enumerated via the GET endpoint. This gating ensures the file explorer can safely read and write files within any worktree without exposing unrelated parts of the host filesystem.

## Branch-Aware UI Components

The interface adapts dynamically based on worktree context. The sidebar—rendered by [`components/SessionSidebar.tsx`](https://github.com/agegr/pi-web/blob/main/components/SessionSidebar.tsx)—displays a *worktree switcher* only when the current session resides at the repository top-level (`project.isTopLevel`). This component, along with [`components/BranchNavigator.tsx`](https://github.com/agegr/pi-web/blob/main/components/BranchNavigator.tsx), lists each worktree’s `path` and `branch` obtained from `listWorktrees()`.

Selecting a different worktree updates the session’s cwd, which automatically triggers `resolveProject()` to re-resolve the project context for the new branch.

## Cache Invalidation on Worktree Changes

Mutating worktree state requires cache synchronization. Adding or removing a worktree invokes `invalidateProjectCache()`, clearing cached `ProjectInfo` entries so subsequent resolves reflect the updated repository structure. Additionally, `invalidateSessionListCache()` clears session-list caches to ensure sidebar grouping remains accurate after worktree modifications.

## Code Examples

Resolve the project information for any directory to determine if it lives inside a worktree:

```typescript
import { resolveProject } from "@/lib/worktree";

const cwd = "/home/alice/myrepo-feature";
const info = await resolveProject(cwd);
// info => { projectRoot: "/home/alice/myrepo", branch: "feature", isWorktree: true, isTopLevel: true }

```

List all existing worktrees for repository navigation:

```typescript
import { listWorktrees } from "@/lib/worktree";

const worktrees = await listWorktrees("/home/alice/myrepo");
console.log(worktrees);
/* [
 *   { path: "/home/alice/myrepo-worktrees/feature", branch: "feature", isMain: true },
 *   { path: "/home/alice/myrepo-worktrees/bugfix",  branch: "bugfix",  isMain: false },
 * ] */

```

Create a new worktree for a feature branch via the API:

```typescript
await fetch("/api/worktrees", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ cwd: "/home/alice/myrepo", branch: "new-feature" })
});
// Returns: { path: "/home/alice/myrepo-worktrees/new-feature", branch: "new-feature" }

```

Remove a worktree, optionally forcing deletion of dirty trees:

```typescript
await fetch("/api/worktrees", {
  method: "DELETE",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ 
    cwd: "/home/alice/myrepo", 
    path: "/home/alice/myrepo-worktrees/old-feature", 
    force: true 
  })
});
// Returns: { success: true }

```

## Summary

- **Automatic detection**: `resolveProject()` in [`lib/worktree.ts`](https://github.com/agegr/pi-web/blob/main/lib/worktree.ts) uses `git rev-parse` to identify worktree relationships and cache results for 60 seconds.
- **Unified grouping**: [`lib/session-reader.ts`](https://github.com/agegr/pi-web/blob/main/lib/session-reader.ts) attaches `projectRoot` and `worktreeBranch` to sessions, enabling the sidebar to group sessions by underlying repository.
- **Secure file access**: The `allowFileRoot()` mechanism in [`lib/file-access.ts`](https://github.com/agegr/pi-web/blob/main/lib/file-access.ts) gates filesystem access, registering worktree paths only when explicitly created or listed.
- **RESTful management**: [`app/api/worktrees/route.ts`](https://github.com/agegr/pi-web/blob/main/app/api/worktrees/route.ts) provides endpoints to create, list, and delete worktrees under `<repoRoot>-worktrees/<branch>`.
- **Dynamic UI**: Components like [`BranchNavigator.tsx`](https://github.com/agegr/pi-web/blob/main/BranchNavigator.tsx) expose branch switching only at the repository top-level, updating session cwd to trigger re-resolution.

## Frequently Asked Questions

### How does pi-web detect if a directory is a Git worktree?

pi-web detects worktrees through the `resolveProject()` function in [`lib/worktree.ts`](https://github.com/agegr/pi-web/blob/main/lib/worktree.ts), which executes `git rev-parse --git-common-dir` to locate the shared `.git` directory. If the query reveals the cwd is a linked worktree rather than the main repository, the function sets `isWorktree` to `true` and extracts the current branch name.

### Where does pi-web create new worktrees when adding a branch?

When calling the POST endpoint at `/api/worktrees`, pi-web’s `addWorktree()` function creates a new directory under `<repoRoot>-worktrees/<sanitized-branch>`. This path is immediately registered with `allowFileRoot()` in [`lib/file-access.ts`](https://github.com/agegr/pi-web/blob/main/lib/file-access.ts) to grant the file explorer access.

### How does pi-web prevent unauthorized file access when browsing worktrees?

pi-web maintains a global allow-list of permitted file roots via `allowFileRoot()`. Worktree paths are added to this list only when explicitly created through `addWorktree()` or enumerated via the GET `/api/worktrees` endpoint, ensuring the UI cannot access arbitrary filesystem locations outside the repository structure.

### Why does the worktree switcher only appear at the repository top-level?

The [`BranchNavigator.tsx`](https://github.com/agegr/pi-web/blob/main/BranchNavigator.tsx) component checks the `isTopLevel` property returned by `resolveProject()` before rendering the switcher. This design prevents nested worktree directories from displaying redundant navigation controls, as the root repository context is required to enumerate all available worktrees through `listWorktrees()`.