CodeVetter · Repo Unpack · Risk map
earendil-works/pi
The dominant risk is intentional: pi is an unsandboxed local agent whose bash/edit/write tools and jiti-loaded extensions run with full user privileges — the docs state this plainly and point to containerization instead. Concrete hotspots: auth.json holds plaintext credentials (mitigated by 0600 + locking); the extension/package system executes third-party code gated only by project trust for project-local sources; RPC/protocol layers have no peer authentication; and giant files (agent-session.ts 4023 lines, interactive-mode.ts 6851 lines, package-manager.ts 2699 lines) concentrate blast radius.
- No built-in sandbox: docs/security.md states built-in tools can read/write files and run shell commands with the pi process's permissions, and extensions run with the same permissions — isolation is delegated to containers/VMs (docs/containerization.md).
- auth.json stores OAuth/API credentials in plaintext JSON under ~/.pi/agent; writes use mode 0600 and a proper-lockfile lock, but any process running as the user (including extensions and bash commands) can read it.
- Extension loader uses jiti to execute arbitrary TypeScript from user dirs, project dirs, npm/git packages, and -e flags; aliases map @earendil-works/*, legacy @mariozechner/* names, and typebox into the host's own modules — so extensions share the host process and its privileges entirely.
- Project trust is the only input-loading guard: .pi/settings.json, .pi/{extensions,skills,prompts,themes}, .pi/SYSTEM.md, and .agents/skills require trust before loading; non-interactive modes never prompt, so defaultProjectTrust 'ask'/'never' skips them — meaning a config mistake can silently auto-trust or a repo's context files (AGENTS.md/CLAUDE.md) always load regardless.
- The bash tool spawns real shells with detached process groups and tracks detached PIDs for killProcessTree; shellCommandPrefix and custom shellPath settings let projects/users alter every command — a footgun if a trusted-but-malicious project sets them.
- Package manager installs npm/git sources into the agent dir and can self-update the pi binary (with Windows quarantine handling); supply-chain mitigations exist (exact-pinned direct deps, min-release-age=2, npm-shrinkwrap for the published CLI, lifecycle-script allowlist in shrinkwrap generation) but the installed packages still execute as the user.
- RPC mode exposes full session control (bash, prompt, fork, switch_session) on stdin/stdout with no authentication — it trusts whoever spawned it; the experimental pi-protocol README explicitly states peer authentication is not implemented by the transport.
- Blast-radius hotspots: agent-session.ts (4023 lines) mixes session lifecycle, extension dispatch, compaction, retry, bash, export; interactive-mode.ts (6851 lines) is the entire TUI; package-manager.ts (2699 lines) does network + fs + spawn — regressions in these files affect every mode. (inferred)
- Truncated tool calls are a real corruption vector deliberately mitigated: when stopReason is 'length', all tool calls are failed with a re-issue error rather than executed, because the JSON salvage parser can produce arguments that validate but are silently incomplete.
- main() calls process.exit() deep inside helper flows (validateForkFlags, createSessionManager error paths, handlePackageCommand) which complicates embedding and can skip cleanup; a comment notes Windows requires natural drain after `pi update` due to a Node fetch() teardown bug.
- models.generated.ts and image-models.generated.ts are machine-generated from live provider catalogs; editing them directly is forbidden — regeneration can pull unrelated upstream metadata changes into a diff, which is explicitly accepted.
- Git safety rules exist because concurrent pi sessions work in one repo: AGENTS.md forbids git add -A, reset --hard, stash, clean -fd, and --no-verify; the pre-commit hook additionally blocks lockfile changes without PI_ALLOW_LOCKFILE_CHANGE=1.
- Provider streaming code paths (api/*.ts, ~1.7k-line openai-completions.ts, bedrock-converse-stream.ts) are largely exercised only when real API keys exist, so provider-specific regressions can slip past CI which runs without keys. (inferred)