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

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 performs the initial assessment:

// 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:

{
  "/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:

// 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.

GET Endpoint: Query Trust Status

// 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

// 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 presents the trust request to users with clear project identification and action buttons.

Dialog Component Structure

// 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:

// 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

// 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

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

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

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 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 (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 only exposes set(cwd, true) through the trustProject function. Direct revocation would require calling ProjectTrustStore.set(cwd, false) or manually editing the trust.json file, as no explicit "untrust" API endpoint is implemented in app/api/project-trust/route.ts.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →