# How ProjectTrustDialog Works in Pi Web: Trust Status Storage and Security Flow

> Understand how Pi Web's ProjectTrustDialog manages trust status, stores decisions in ProjectTrustStore, and secures user code execution. Learn the storage and security flow.

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

---

**The ProjectTrustDialog in Pi Web gates execution of user-provided code by checking whether a project requires trust, persisting the user's decision in a JSON-based ProjectTrustStore under `~/.pi/agent`, and blocking resource loading until trust is explicitly granted.**

Pi Web implements a comprehensive security model where the **ProjectTrustDialog** serves as the primary interface for user consent before executing potentially unsafe code. This article examines the complete trust flow—from detection and UI presentation to persistent storage—based on the `agegr/pi-web` source code.

## How Project Trust Detection Works

Before displaying the **ProjectTrustDialog**, Pi Web determines whether a project actually requires trust. This prevents unnecessary interruptions for safe projects.

### Detecting Trust-Requiring Resources

The `getProjectTrustStatus` function in [`lib/project-trust.ts`](https://github.com/agegr/pi-web/blob/main/lib/project-trust.ts) performs the initial assessment:

```ts
// lib/project-trust.ts
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,
  };
}

```

The helper `hasTrustRequiringProjectResources` scans the project directory for:
- `.pi/extensions` — project-specific extensions
- Project-specific settings files
- `.agents/skills` — custom agent skills

If none of these gated resources exist, the project is automatically considered **trusted** without user intervention.

## ProjectTrustStore: The Trust Status Storage Mechanism

The **ProjectTrustStore** class from `@earendil-works/pi-coding-agent` provides the authoritative storage layer for trust decisions. It persists data as a JSON map under the agent directory.

### Storage Location and Format

By default, trust decisions are written to `~/.pi/agent/trust.json`. The file contains a simple mapping of project paths to boolean values:

```json
{
  "/home/user/projects/my-agent": true,
  "/home/user/projects/suspicious-code": false
}

```

### Persisting Trust Decisions

When a user clicks **Trust** in the dialog, the backend writes the decision via `trustProject`:

```ts
// lib/project-trust.ts
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 };
}

```

This persistence mechanism ensures trust decisions survive across browser sessions, application restarts, and window reloads.

## API Endpoints for Project Trust Status

Pi Web exposes trust status through dedicated Next.js API routes in [`app/api/project-trust/route.ts`](https://github.com/agegr/pi-web/blob/main/app/api/project-trust/route.ts).

### GET Endpoint: Query Trust Status

```ts
// app/api/project-trust/route.ts
export async function GET(req: Request) {
  const result = await validateCwd(new URL(req.url).searchParams.get("cwd"));
  if ("response" in result) return result.response;
  return NextResponse.json(getProjectTrustStatus(result.cwd, getAgentDir()));
}

```

Returns:
- `requiresTrust: boolean` — whether the project contains gated resources
- `trusted: boolean` — current trust state from `ProjectTrustStore`

### POST Endpoint: Grant Trust

```ts
// app/api/project-trust/route.ts
export async function POST(req: Request) {
  const body = await req.json() as { cwd?: unknown };
  const result = await validateCwd(body.cwd);
  if ("response" in result) return result.response;
  const status = trustProject(result.cwd, getAgentDir());
  invalidateModelsCache();
  await destroyRpcSessionsForCwd(result.cwd);
  return NextResponse.json(status);
}

```

The POST handler performs cleanup after granting trust: it invalidates the models cache and destroys existing RPC sessions for the project, ensuring a fresh security context.

## ProjectTrustDialog UI Implementation

The `ProjectTrustDialog` component in [`components/ProjectTrustDialog.tsx`](https://github.com/agegr/pi-web/blob/main/components/ProjectTrustDialog.tsx) presents the trust request to users with clear project identification and action buttons.

### Dialog Component Structure

```tsx
// components/ProjectTrustDialog.tsx
export function ProjectTrustDialog({ cwd, busy, error, onCancel, onConfirm }) {
  const { t } = useI18n();
  return (
    <div role="presentation" … onClick={(e)=>!busy && e.target===e.currentTarget && onCancel()}>
      <div role="dialog" aria-modal="true" …>
        <div>
          <div id="project-trust-title">{t("trust.dialogTitle")}</div>
          <div>{t("trust.dialogBody")}</div>
          <code>{cwd}</code>
          {error && <div role="alert">{error}</div>}
        </div>
        <button onClick={onCancel} disabled={busy}>{t("trust.cancel")}</button>
        <button onClick={onConfirm} disabled={busy}>
          {busy ? t("trust.trusting") : t("trust.trustProject")}
        </button>
      </div>
    </div>
  );
}

```

The dialog includes:
- **Project path display** — shows the exact directory being trusted
- **Loading states** — `busy` prop prevents duplicate submissions
- **Error handling** — displays backend validation failures
- **Keyboard accessibility** — standard dialog roles and modal behavior

### Conditional Rendering in AppShell

The `AppShell` component orchestrates when the **ProjectTrustDialog** appears:

```tsx
// components/AppShell.tsx
useEffect(() => {
  setProjectTrust(null);
  setProjectTrustDialogOpen(false);
  if (!projectTrustCwd) return;
  fetch(`/api/project-trust?cwd=${encodeURIComponent(projectTrustCwd)}`)
    .then(r => r.json())
    .then(data => setProjectTrust(data))
    .catch(err => console.error(err));
}, [projectTrustCwd]);

// Render
{projectTrustDialogOpen && projectTrustCwd && (
  <ProjectTrustDialog
    cwd={projectTrustCwd}
    busy={projectTrustBusy}
    error={projectTrustError}
    onCancel={() => setProjectTrustDialogOpen(false)}
    onConfirm={() => void handleTrustProject()}
  />
)}

```

The dialog opens automatically when `projectTrust.requiresTrust && !projectTrust.trusted`.

## Gating Resource Loading with Project Trust

The **trust status storage mechanism** protects all project-local resource loading through `projectTrustReloadOptions`.

### Trust-Based Resource Gate

```ts
// lib/project-trust.ts
export function projectTrustReloadOptions(cwd: string, agentDir: string) {
  const status = getProjectTrustStatus(cwd, agentDir);
  if (!status.requiresTrust) return undefined;
  const trustStore = new ProjectTrustStore(agentDir);
  return { resolveProjectTrust: async () => trustStore.get(cwd) === true };
}

```

Resource loaders like `rpc-manager` and `skills-service` call this helper to obtain a `resolveProjectTrust` promise. They await resolution before:
- Loading extensions
- Initializing RPC sessions
- Executing project-specific skills

This ensures **untrusted projects cannot execute arbitrary code** even if other security checks are bypassed.

## Practical Code Examples

### Client-Side Trust Status Hook

```tsx
import { useEffect, useState } from "react";

function useProjectTrust(cwd: string) {
  const [status, setStatus] = useState<ProjectTrustStatus | null>(null);
  useEffect(() => {
    fetch(`/api/project-trust?cwd=${encodeURIComponent(cwd)}`)
      .then(r => r.json())
      .then(setStatus)
      .catch(console.error);
  }, [cwd]);
  return status;
}

```

### Programmatic Trust Granting

```ts
async function trustProject(cwd: string) {
  const resp = await fetch("/api/project-trust", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ cwd }),
  });
  const data = await resp.json();
  if (!resp.ok) throw new Error(data.error ?? "Failed");
  return data; // { requiresTrust: true, trusted: true }
}

```

### Protected Resource Loading

```ts
import { projectTrustReloadOptions } from "@/lib/project-trust";

async function loadExtensions(cwd: string) {
  const reloadOpts = projectTrustReloadOptions(cwd, getAgentDir());
  if (reloadOpts) {
    const trusted = await reloadOpts.resolveProjectTrust();
    if (!trusted) throw new Error("Project not trusted");
  }
  // safe to import extensions here …
}

```

## Summary

- **Project trust detection** in [`lib/project-trust.ts`](https://github.com/agegr/pi-web/blob/main/lib/project-trust.ts) scans for gated resources (extensions, settings, skills) and consults `ProjectTrustStore` to determine if user interaction is required.

- **Trust status storage mechanism** persists decisions as a JSON map at `~/.pi/agent/trust.json`, keyed by absolute project path, enabling trust decisions to survive across sessions.

- **API endpoints** (`GET` and `POST /api/project-trust`) expose trust operations to the frontend with proper validation and cleanup handling.

- **ProjectTrustDialog** renders conditionally based on `AppShell` state, presenting the project path and capturing explicit user consent.

- **Resource gating** through `projectTrustReloadOptions` ensures no code execution occurs until trust is obtained from the store, as implemented in `rpc-manager` and related services.

## Frequently Asked Questions

### Where is the ProjectTrustDialog defined in the Pi Web codebase?

The **ProjectTrustDialog** is defined in [`components/ProjectTrustDialog.tsx`](https://github.com/agegr/pi-web/blob/main/components/ProjectTrustDialog.tsx) (lines 5-42). It accepts `cwd`, `busy`, `error`, `onCancel`, and `onConfirm` props, rendering an accessible modal dialog with the project path and trust action buttons.

### How does the trust status storage mechanism work across browser restarts?

The **ProjectTrustStore** class writes to a JSON file at `~/.pi/agent/trust.json` by default. This file-based storage survives browser restarts, application updates, and machine reboots because it lives in the user's home directory outside any cache or temporary storage.

### What triggers the ProjectTrustDialog to appear?

The dialog appears when three conditions are met: the current working directory (`cwd`) contains trust-requiring resources (detected by `hasTrustRequiringProjectResources`), the `ProjectTrustStore` has no positive entry for that path, and the user navigates to or opens that project in `AppShell`.

### Can trust decisions be revoked programmatically?

The source code in [`lib/project-trust.ts`](https://github.com/agegr/pi-web/blob/main/lib/project-trust.ts) only exposes `set(cwd, true)` through the `trustProject` function. Direct revocation would require calling `ProjectTrustStore.set(cwd, false)` or manually editing the [`trust.json`](https://github.com/agegr/pi-web/blob/main/trust.json) file, as no explicit "untrust" API endpoint is implemented in [`app/api/project-trust/route.ts`](https://github.com/agegr/pi-web/blob/main/app/api/project-trust/route.ts).