How Project-Trust Validation Prevents Unauthorized File Access in Pi Web
Pi Web prevents unauthorized file access through project-trust validation by combining two independent safeguards: a trust gating system that requires explicit user approval for projects containing potentially dangerous resources, and a path-allowlist enforcement that restricts all file operations to pre-approved workspace roots.
Every file-related API in the Pi Web repository enforces security through a layered defense mechanism. This architecture ensures that even if one control is bypassed, a second barrier remains in place. The system is implemented across lib/project-trust.ts, lib/file-access.ts, and individual route handlers in the app/api/ directory.
The Two-Layer Security Model
Layer 1: Project-Trust Gating
Before any operation touches project files, the route evaluates whether the current working directory is trusted. This check is performed by getProjectTrustStatus(cwd, agentDir) in [lib/project-trust.ts](https://github.com/agegr/pi-web/blob/main/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,
};
}
- Trust-requiring resources include
.pi/extensions, project-local skills, and plugins — any executable code that could compromise the system. - Projects without these resources pass automatically (
requiresTrust: false). - Projects with these resources return
trusted: falseuntil the user explicitly trusts them via the UI orPOST /api/project-trust. - Routes reject untrusted projects with HTTP 403 Forbidden.
Layer 2: Path-Allowlist Enforcement
Even trusted projects cannot access arbitrary files. The isFilePathAllowed function in [lib/file-access.ts](https://github.com/agegr/pi-web/blob/main/lib/file-access.ts) performs a pure lexical containment check:
export function isFilePathAllowed(target: string, allowedRoots: Set<string>): boolean {
return isPathWithinRoots(target, allowedRoots);
}
The allowedRoots set is computed by getAllowedFileRoots(), which aggregates:
- All session current working directories
- All project roots
- Automatically created
~/pi-cwd-*directories
Only paths falling under these roots are reachable. This prevents path traversal attacks even against fully trusted projects.
Step-by-Step Request Flow
Each file API request passes through five verification stages:
-
Origin validation —
isApiRequestAllowedin [lib/request-security.ts](https://github.com/agegr/pi-web/blob/main/lib/request-security.ts) verifies the request comes from a permitted host/origin, blocking CSRF and cross-site abuse. -
Working directory resolution — The route extracts
cwdfrom URL parameters or request body to identify the target project. -
Project-trust check —
getProjectTrustStatus(cwd, agentDir)is invoked. IfrequiresTrust && !trusted, the request is rejected with 403. -
Path-allowlist validation —
isFilePathAllowed(targetPath, allowedRoots)ensures the target path resides within the computed allowed roots. External paths trigger 403. -
File operation execution — Only after passing both the trust gate and path-allowlist check does the handler perform the read or write.
Real-World Implementation: Files API
The route at app/api/files/[...path]/route.ts demonstrates this pattern in practice:
// Simplified excerpt showing the security sequence
export async function GET(request: NextRequest, { params }: { params: Promise<{ path: string[] }> }) {
// 1. Validate request origin
if (!isApiRequestAllowed(request)) {
return NextResponse.json({ error: "Untrusted API request" }, { status: 403 });
}
const { path: segments } = await params;
const cwd = await resolveCwd(segments);
const trust = getProjectTrustStatus(cwd, getAgentDir());
// 2. Enforce project-trust validation
if (!trust.trusted) {
return NextResponse.json({ error: "Project not trusted" }, { status: 403 });
}
// 3. Validate path against allowlist
const filePath = resolveFilePath(cwd, segments);
const allowedRoots = await getAllowedFileRoots();
if (!isFilePathAllowed(filePath, allowedRoots)) {
return NextResponse.json({ error: "Path not allowed" }, { status: 403 });
}
// 4. Execute safe file read
const content = await readFile(filePath, "utf-8");
return new NextResponse(content);
}
This same pattern appears in:
- Worktree API — git worktree operations
- Git status/diff APIs — repository inspection
- Skill-install API — [
app/api/skills/install/route.ts](https://github.com/agegr/pi-web/blob/main/app/api/skills/install/route.ts) aborts with 403 when the project lacks trust
Trust Store Persistence
The ProjectTrustStore class manages persistent trust decisions. Trust is stored per-project and survives across sessions. The store location is determined by agentDir, ensuring isolation between different Pi Web agent instances.
Users grant trust through:
- The web interface trust prompt
- Direct
POST /api/project-trustAPI call with the project path
Once granted, trust persists until explicitly revoked.
Summary
Project-trust validation in Pi Web prevents unauthorized file access through:
- Explicit trust requirements for projects containing executable resources (extensions, skills, plugins)
- Dual-gate verification — both trust status and path containment must pass
- Lexical path containment against computed allowed roots, blocking traversal attacks
- Consistent enforcement across all file-touching APIs including files, git operations, and skill installation
- Request origin validation as a foundational first line of defense
Frequently Asked Questions
What triggers a project to require trust?
A project requires trust when hasTrustRequiringProjectResources(cwd) detects .pi/extensions directories, project-local skills, or plugin configurations. These resources can execute arbitrary code, so Pi Web treats them as potentially dangerous until explicitly approved.
Can a trusted project access files outside its workspace?
No. Even fully trusted projects are constrained by the path-allowlist check in lib/file-access.ts. The isFilePathAllowed function validates that every target path falls within the pre-computed allowedRoots set, which includes only the project's own directories and explicitly managed session roots.
How does project-trust validation differ from traditional authentication?
Project-trust validation operates after origin verification but before file operations, serving as an authorization layer specific to filesystem access. It does not replace authentication — rather, it adds a user-consent requirement for projects that could execute code, complementing the origin-based isApiRequestAllowed check.
Where is the trust status stored?
Trust decisions persist in a ProjectTrustStore instance initialized with the agentDir path. The store provides get(cwd) and set(cwd, trusted) operations, enabling durable trust state across sessions while maintaining isolation between different agent installations.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →