# How Project Trust and Project Identity Work in Pi Web: Security and Session Grouping Explained

> Discover how project trust and project identity in Pi Web ensure secure local resource execution via user consent and correct session grouping. Learn about ProjectTrustStore and projectIdentityKey.

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

---

**Project trust and project identity in Pi Web work together to gate the execution of local resources through explicit user consent while ensuring sessions group correctly across different filesystem paths, implemented via `ProjectTrustStore` and `projectIdentityKey` respectively.**

The `agegr/pi-web` codebase implements a dual-layer system to handle potentially unsafe project-local code and cross-platform session organization. **Project trust** determines whether extensions, custom settings, and skills may execute, while **project identity** provides a stable, normalized key for grouping sessions regardless of OS-specific path variations.

## Project Trust: Gating Local Resource Execution

Pi Web treats any repository containing **trust-requiring resources** as potentially unsafe. These resources include `.pi/extensions` directories, project-wide [`.pi/settings.json`](https://github.com/agegr/pi-web/blob/main/.pi/settings.json) entries, or `.agents/skills` folders. The system stores trust decisions in the user's `~/.pi/agent` directory via the SDK's `ProjectTrustStore`.

### Detecting Trust Requirements

The `getProjectTrustStatus` function in [`lib/project-trust.ts`](https://github.com/agegr/pi-web/blob/main/lib/project-trust.ts) checks for gated resources and returns whether the project requires trust and whether it has been granted:

```typescript
// getProjectTrustStatus → lines 4-13
export function getProjectTrustStatus(cwd: string, agentDir: string): ProjectTrustStatus {
  const requiresTrust = Boolean(cwd) && hasTrustRequiringProjectResources(cwd);
  if (!requiresTrust) return { requiresTrust: false, trusted: true };
  const trustStore = new ProjectTrustStore(agentDir);
  return {
    requiresTrust: true,
    trusted: trustStore.get(cwd) === true,
  };
}

```

If `hasTrustRequiringProjectResources` finds no gated files, the project is implicitly trusted. Otherwise, the function queries the `ProjectTrustStore` for the current working directory.

### Granting and Revoking Trust

The `trustProject` function writes the user's consent to the store:

```typescript
// trustProject → lines 15-21
export function trustProject(cwd: string, agentDir: string): ProjectTrustStatus {
  const status = getProjectTrustStatus(cwd, agentDir);
  if (!status.requiresTrust) return status;
  new ProjectTrustStore(agentDir).set(cwd, true);
  return { requiresTrust: true, trusted: true };
}

```

Once persisted, subsequent calls to `getProjectTrustStatus` return `trusted: true` for that path.

### Trust Integration in the Session Lifecycle

When an `AgentSession` starts, Pi Web supplies trust gating through `projectTrustReloadOptions` in [`lib/project-trust.ts`](https://github.com/agegr/pi-web/blob/main/lib/project-trust.ts):

```typescript
// projectTrustReloadOptions → lines 40-48
export function projectTrustReloadOptions(
  cwd: string,
  agentDir: string,
): { resolveProjectTrust: () => Promise<boolean> } | undefined {
  const status = getProjectTrustStatus(cwd, agentDir);
  if (!status.requiresTrust) return undefined;
  const trustStore = new ProjectTrustStore(agentDir);
  return { resolveProjectTrust: async () => trustStore.get(cwd) === true };
}

```

The [`rpc-manager.ts`](https://github.com/agegr/pi-web/blob/main/rpc-manager.ts) file uses these options when constructing a `DefaultResourceLoader`. If `resolveProjectTrust` returns `false`, extensions remain dormant. After every reload, `syncProjectTrust` synchronizes the flag with the session's `SettingsManager` so UI components can display trust status:

```typescript
// syncProjectTrust → lines 60-63
private syncProjectTrust(): void {
  const status = getProjectTrustStatus(this.cwd, getAgentDir());
  this.inner.settingsManager.setProjectTrusted(status.trusted);
}

```

## Project Identity: Stable Grouping Across Filesystems

Sessions must group logically by project even when filesystem representations differ. Windows paths are case-insensitive and may contain trailing separators, while Git worktrees can reference the same repository through different physical directories.

### Normalizing Path Variations

The `projectIdentityKey` function in [`lib/project-identity.ts`](https://github.com/agegr/pi-web/blob/main/lib/project-identity.ts) generates a stable identity string:

```typescript
// projectIdentityKey → lines 12-26
export function projectIdentityKey(
  projectRoot: string,
  platform: NodeJS.Platform = process.platform,
): string {
  if (!projectRoot) return projectRoot;
  const pathApi = platform === "win32" ? path.win32 : path.posix;
  const normalized = pathApi.normalize(projectRoot);
  const rootLength = pathApi.parse(normalized).root.length;
  let end = normalized.length;
  while (end > rootLength && normalized[end - 1] === pathApi.sep) end--;
  const withoutTrailingSeparators = normalized.slice(0, end);
  return platform === "win32"
    ? withoutTrailingSeparators.toLowerCase()
    : withoutTrailingSeparators;
}

```

This function handles three normalization steps:

1. **Path normalization** using the appropriate platform API
2. **Trailing separator removal** while preserving the root (e.g., `C:\`)
3. **Case folding** for Windows platforms (`toLowerCase()`)

### Identity in Session Management

The [`session-reader.ts`](https://github.com/agegr/pi-web/blob/main/session-reader.ts) file attaches `projectKey` to every `SessionInfo` object:

```typescript
// attachSessionProjectInfo → lines 29-33
const projectRoot = project?.projectRoot ?? session.cwd;
return {
  ...session,
  projectRoot,
  projectKey: projectIdentityKey(projectRoot),
  ...(project?.isWorktree && project.branch ? { worktreeBranch: project.branch } : {}),
};

```

The UI uses this key for sidebar grouping rather than raw paths.

### Identity in API Responses

The worktree API in [`app/api/worktrees/route.ts`](https://github.com/agegr/pi-web/blob/main/app/api/worktrees/route.ts) exposes `projectKey` to clients:

```typescript
// worktrees route → lines 44-46
return NextResponse.json({
  projectRoot: project.projectRoot,
  projectKey: projectIdentityKey(project.projectRoot),
  …
});

```

This allows client-side caching per logical project rather than per physical path.

## Interaction Between Trust and Identity

When a user opens a repository, Pi Web executes a two-phase process:

1. **Identity Resolution**: `projectIdentityKey` derives a stable key so the UI places the session in the correct project bucket, regardless of worktree path variations.
2. **Trust Evaluation**: If gated resources exist, the UI displays a "Trust required" prompt. The trust store is keyed by the **raw cwd**, while the UI groups by **projectKey**.
3. **Inheritance**: Once the user clicks "Trust", `trustProject` persists the decision. Subsequent sessions for any path resolving to the same project identity automatically inherit the trusted state because the store lookup uses the specific path while the UI presentation uses the normalized identity.

This architecture ensures that trusting a project in one worktree effectively trusts it for all worktrees of the same repository.

## Summary

- **Project trust** gates execution of `.pi/extensions`, [`.pi/settings.json`](https://github.com/agegr/pi-web/blob/main/.pi/settings.json), and `.agents/skills` via `ProjectTrustStore` in [`lib/project-trust.ts`](https://github.com/agegr/pi-web/blob/main/lib/project-trust.ts).
- **`getProjectTrustStatus`** checks for gated resources; **`trustProject`** writes consent to `~/.pi/agent`.
- **Project identity** provides OS-agnostic grouping keys via `projectIdentityKey` in [`lib/project-identity.ts`](https://github.com/agegr/pi-web/blob/main/lib/project-identity.ts), handling Windows case-insensitivity and trailing separators.
- **[`rpc-manager.ts`](https://github.com/agegr/pi-web/blob/main/rpc-manager.ts)** synchronizes trust status into running sessions, while **[`session-reader.ts`](https://github.com/agegr/pi-web/blob/main/session-reader.ts)** and **[`app/api/worktrees/route.ts`](https://github.com/agegr/pi-web/blob/main/app/api/worktrees/route.ts)** consume identity keys for UI grouping.
- Trust decisions persist across worktrees because the store keys by raw path while the UI groups by normalized identity.

## Frequently Asked Questions

### How does Pi Web decide if a project requires trust?

Pi Web scans the repository root for **trust-requiring resources** using `hasTrustRequiringProjectResources`. If the project contains `.pi/extensions`, project-wide [`.pi/settings.json`](https://github.com/agegr/pi-web/blob/main/.pi/settings.json) entries, or `.agents/skills` directories, `getProjectTrustStatus` returns `requiresTrust: true` and checks the `ProjectTrustStore` for user consent.

### What happens if I decline to trust a project?

If `resolveProjectTrust` returns `false` in the `projectTrustReloadOptions` callback, the `DefaultResourceLoader` loads extensions in a dormant state. The `AgentSession` treats the project as untrusted, and local skills or custom settings will not execute, though the session remains functional for basic operations.

### Why does Pi Web use a project identity key instead of the raw file path?

Raw filesystem paths fail to group sessions correctly across platform differences. Windows paths vary by case and may include trailing backslashes, while Git worktrees create multiple physical directories for one logical repository. The `projectIdentityKey` function normalizes these variations into a stable string, ensuring the sidebar groups all worktrees of the same project under a single entry.

### Where is the trust status stored on disk?

The `ProjectTrustStore` persists the trust boolean in the user's `~/.pi/agent` directory, keyed by the canonical working directory path. This location is separate from the project repository, allowing trust decisions to persist across git operations or directory moves as long as the path remains consistent.