# How DirectoryPicker Validates and Selects Working Directories in pi-web

> Learn how pi-web's DirectoryPicker validates and selects working directories. Discover the two-stage process involving client-side browsing and server-side security checks for robust path management.

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

---

**The pi-web DirectoryPicker uses a two-stage validation process where the React component handles interactive directory browsing via `GET /api/cwd/browse`, while the server-side `POST /api/cwd/validate` endpoint performs rigorous path normalization, existence checks, and security registration through `allowFileRoot()` before accepting a working directory.**

The pi-web repository implements a secure workspace selection system that separates UI navigation from filesystem validation. Located in [`components/DirectoryPicker.tsx`](https://github.com/agegr/pi-web/blob/main/components/DirectoryPicker.tsx), the component coordinates with Next.js API routes to ensure only existing directories with proper permissions become active session roots.

## Client-Side Directory Browsing

The DirectoryPicker component manages the interactive file browser interface, handling navigation state and user interactions while deferring final validation to the server.

### Loading Directory Contents

Directory listings are fetched through the `loadDirectories` function, which communicates with the browse endpoint. This async function constructs query parameters from the optional `dir` argument and returns a `BrowseResponse` containing the current path, parent reference, and child entries.

```tsx
// components/DirectoryPicker.tsx
async function loadDirectories(dir?: string): Promise<BrowseResponse> {
  const query = dir ? `?path=${encodeURIComponent(dir)}` : "";
  const resp = await fetch(`/api/cwd/browse${query}`);
  const data = await resp.json() as BrowseResponse;
  if (!resp.ok || data.error) throw new Error(data.error ?? `HTTP ${resp.status}`);
  return data;
}

```

The response payload populates the UI with clickable directory rows. When users select an entry, the `navigateTo(entry.path)` function updates the component state and triggers a new fetch for the next directory level.

### Navigation and Selection State

The **Select** button becomes active only through the `canSelect` boolean logic, which verifies that the displayed `currentPath` matches the text entered in the path input field. This guard prevents submission of paths the user has not explicitly navigated to or confirmed.

```tsx
// Triggered when user clicks the Select button
<button
  type="button"
  onClick={() => onSelect(currentPath)}
  disabled={!canSelect}
>
  {t("directoryPicker.selectThisFolder")}
</button>

```

Once enabled, clicking **Select** invokes `onSelect(currentPath)`, passing the chosen directory back to the parent component (typically the New Session dialog) for submission to the validation API.

## Server-Side Path Validation

The [`app/api/cwd/validate/route.ts`](https://github.com/agegr/pi-web/blob/main/app/api/cwd/validate/route.ts) file implements the final authority for directory acceptance, performing three distinct validation layers before registering the path as a permitted file root.

### Path Normalization

The `normalizeCwd` utility function processes raw input to handle shell shortcuts and relative references. It expands tilde notation (`~` and `~/`) to the user's home directory and resolves relative paths against the process current working directory, ensuring filesystem operations receive absolute paths.

### Existence and Type Checks

The POST handler uses **synchronous filesystem assertions** to verify the candidate path. It calls `statSync(normalizedCwd)` within a try-catch block to confirm existence, then checks `stat.isDirectory()` to reject file paths.

```ts
// app/api/cwd/validate/route.ts
export async function POST(req: Request) {
  const body = await req.json() as { cwd?: unknown };
  const cwd = typeof body.cwd === "string" ? body.cwd.trim() : "";

  if (!cwd) return NextResponse.json({ error: "Path is required" }, { status: 400 });

  const normalizedCwd = normalizeCwd(cwd);
  let stat: Stats;
  try { stat = statSync(normalizedCwd); }
  catch { return NextResponse.json({ error: `Directory does not exist: ${cwd}` }, { status: 400 }); }

  if (!stat.isDirectory())
    return NextResponse.json({ error: `Path is not a directory: ${cwd}` }, { status: 400 });
  
  // Registration happens here...
}

```

If either check fails, the endpoint returns a 400 status with a descriptive error message before any filesystem registration occurs.

### Registering Allowed Roots

Upon validation success, the handler calls `allowFileRoot(normalizedCwd)` from [`lib/allowed-roots.ts`](https://github.com/agegr/pi-web/blob/main/lib/allowed-roots.ts). This function updates the `globalThis.__piAllowedRootsCache` Set, adding the normalized path to an in-memory whitelist of permitted file roots.

The whitelist acts as a security boundary for the session. Subsequent calls to file-access APIs in [`lib/file-access.ts`](https://github.com/agegr/pi-web/blob/main/lib/file-access.ts) consult this cache via `getAdditionalAllowedRoots()`, rejecting requests that attempt to read files outside registered directories. This architecture ensures that even if the client-side UI is compromised, the server maintains strict filesystem isolation.

## Security Integration with File Access APIs

The validation process directly feeds the security model implemented in [`lib/file-access.ts`](https://github.com/agegr/pi-web/blob/main/lib/file-access.ts). When the DirectoryPicker confirms a working directory, the `allowFileRoot()` registration immediately grants the session permission to read files within that tree.

The file-access module queries the whitelist through `getAdditionalAllowedRoots()` before serving any file contents. If a request targets a path not contained within registered roots, the API returns a 403 Forbidden response. This defense-in-depth strategy ensures that users can only access files within directories they explicitly selected and validated through the DirectoryPicker workflow.

## Summary

- **Client-side browsing** in [`components/DirectoryPicker.tsx`](https://github.com/agegr/pi-web/blob/main/components/DirectoryPicker.tsx) uses `loadDirectories()` to fetch entries from `GET /api/cwd/browse`, with `canSelect` guarding the submission button.
- **Server validation** in [`app/api/cwd/validate/route.ts`](https://github.com/agegr/pi-web/blob/main/app/api/cwd/validate/route.ts) normalizes paths with `normalizeCwd()`, verifies existence via `statSync`, and confirms directory status with `isDirectory()`.
- **Security registration** through `allowFileRoot()` in [`lib/allowed-roots.ts`](https://github.com/agegr/pi-web/blob/main/lib/allowed-roots.ts) maintains the `globalThis.__piAllowedRootsCache` whitelist used by [`lib/file-access.ts`](https://github.com/agegr/pi-web/blob/main/lib/file-access.ts) for runtime permission enforcement.
- **Dual-layer protection** ensures the UI provides a smooth navigation experience while the server guarantees only real, accessible directories become active working roots.

## Frequently Asked Questions

### What happens if a user submits a path to a file instead of a directory?

The server-side validation in [`app/api/cwd/validate/route.ts`](https://github.com/agegr/pi-web/blob/main/app/api/cwd/validate/route.ts) explicitly checks `stat.isDirectory()` after calling `statSync()`. If the path points to a file rather than a directory, the endpoint returns a 400 Bad Request response with the message `"Path is not a directory"` and does not register the path in the allowed roots cache.

### How does pi-web handle home directory shortcuts like tilde (~)?

The `normalizeCwd` function processes the raw path string before validation, expanding `~` or `~/` prefixes to the user's actual home directory path. This normalization ensures that `statSync` and subsequent file operations receive absolute paths, preventing errors from relative or shell-style references.

### Why does the DirectoryPicker disable the Select button during navigation?

The `canSelect` boolean requires the text input value to match the `currentPath` state derived from API responses. This prevents users from submitting paths they typed manually but never confirmed through the browser interface, reducing errors from typos or non-existent directories before the server validation step.

### Where does the system store permitted working directories?

Validated directories are stored in the global variable `globalThis.__piAllowedRootsCache`, a Set managed by [`lib/allowed-roots.ts`](https://github.com/agegr/pi-web/blob/main/lib/allowed-roots.ts). This in-memory whitelist is checked by [`lib/file-access.ts`](https://github.com/agegr/pi-web/blob/main/lib/file-access.ts) on every file read request to enforce that sessions cannot access files outside their selected working directories.