Settings, Trust, Telemetry & Output Guard
Project trust gate & TUI output integrity
“output-guard owns stdout so the model never corrupts the TUI”
Deep Dive
output-guard takes over stdout so the model never breaks the TUI
Learning Objectives
After this chapter you will be able to:
- Explain the two-layer settings (global + project) merge and hot-reload mechanism
- Describe the complete project trust gate flow and its security boundaries
- Explain how output-guard cooperates with the render cycle to guarantee frame integrity
- Understand the telemetry opt-out mechanism and privacy boundaries
Key Source Files
| File | Lines | Responsibility |
|---|---|---|
packages/coding-agent/src/core/settings-manager.ts | 1234 | Two-layer settings merge, hot-reload, migrations |
packages/coding-agent/src/core/trust-manager.ts | ~300 | Trust state management |
packages/coding-agent/src/core/project-trust.ts | ~200 | resolveProjectTrusted implementation |
packages/coding-agent/src/core/telemetry.ts | ~250 | Telemetry reporting and opt-out |
packages/coding-agent/src/core/output-guard.ts | ~150 | stdout takeover and buffering |
packages/coding-agent/src/core/http-dispatcher.ts | ~100 | undici dispatcher + proxy configuration |
Design Motivation: Why Are Settings So Complex?
Problem: pi's configuration comes from multiple dimensions:
- User global preferences (~/.pi/agent/settings.json)
- Project-specific config (.pi/settings.json)
- One-shot CLI overrides (--model, --thinking)
- Runtime hot-modification (/settings dialog)
If there were only one layer: Either global config pollutes projects ("I set vim keybindings globally, but this project needs emacs"), or project config cannot override global.
Analogy: Like Git's three-layer config — system → global → local, where closer means higher priority. pi uses global → project → CLI.
Design goals: Merge deterministically, reload without restart, migrate without data loss.
Two-Layer Settings Hot-Reload
class SettingsManager {
private global: Settings // ~/.pi/agent/settings.json
private project: Settings // .pi/settings.json
get merged(): Settings {
return deepMerge(this.global, this.project) // project overrides global
}
// Hot-reload: automatically reload on file change
watch() {
fsWatch("~/.pi/agent/settings.json", () => this.reload())
fsWatch(".pi/settings.json", () => this.reload())
}
// Migration: old-version settings auto-upgrade
migrate() {
if (this.global.version < CURRENT_VERSION) {
migrations[this.global.version](this.global)
warn("settings auto-migrated to v" + CURRENT_VERSION)
}
}
}
Why hot-reload matters: Users never restart pi after changing settings. Edit the file → next frame picks it up. The /settings dialog internally writes the file → triggers hot-reload.
Project Trust Gate
resolveProjectTrusted(cwd):
① cwd found in ~/.pi/agent/trust.json? → trusted ✓
② .pi/trust.json exists?
├── User confirms → write trust.json → trusted ✓
└── User declines → untrusted ✗
③ Default policy (defaultProjectTrust):
├── "ask" (default) → prompt dialog
├── "trust" → auto-trust (dangerous!)
└── "deny" → auto-deny (safest)
Restrictions when untrusted:
.pi/extensions/not loaded (→ Chapter 13).pi/skills/not loaded (→ Chapter 12).pi/SYSTEM.mdnot applied (→ Chapter 12)- But global extensions and AGENTS.md still load (not controlled by the project)
Why do global extensions load before trust? They are user-installed (pi install), not project-controlled — they may participate in trust event handling (e.g., enterprise compliance policy injection).
output-guard and the Render Cycle
// output-guard.ts
class OutputGuard {
private buffer: string[] = []
install() {
const original = process.stdout.write.bind(process.stdout)
process.stdout.write = (chunk: any, ...args: any[]) => {
this.buffer.push(chunk.toString()) // buffer, don't write directly
return true
}
}
// Called by the render loop at a safe point
flush(write: (s: string) => void) {
const content = this.buffer.join("")
this.buffer = []
if (content) write(content) // controlled write-out
}
}
Render cycle cooperation (→ Chapter 17): ① Collect events → ② Update components → ③ output-guard.flush() writes subprocess output into message panel → ④ Differential render wrapped in DECSET 2026. Subprocess output is "recruited" as TUI content at step ③, never tearing the frame.
Telemetry and Privacy
// telemetry.ts
class Telemetry {
enabled = process.env.PI_TELEMETRY !== "0"
reportInstall() {
if (!this.enabled) return
fetch("https://pi.dev/api/report-install", {
method: "POST",
body: JSON.stringify({ version: PI_VERSION, platform: process.platform })
})
}
}
Privacy boundaries: PI_TELEMETRY=0 disables completely. Only reports install event (version + platform), never conversation content. Attribution header (x-pi-version) tells providers the request came from pi.
Engineering Insights
-
settings-manager.ts is 1234 lines: Mostly schema validation (TypeBox) + migration scripts. Every settings format change adds a migration — users never lose config due to upgrades.
-
output-guard is only ~150 lines: Minimal but critical. It is the secret to "6046 lines of interactive-mode staying stable" — without it, any subprocess console.log is a ticking time bomb.
-
http-dispatcher ~100 lines: Globally configures undici connection pooling, proxy (http_proxy/https_proxy), and timeouts. All HTTP requests (model API, OAuth, telemetry) share this single dispatcher.
Cross-Chapter Links
| Direction | Chapter | Relationship |
|---|---|---|
| Prerequisite | Ch10 CLI Modes | Trust gate executed in main.ts |
| Prerequisite | Ch16 Interactive TUI | output-guard protects TUI frames |
| Prerequisite | Ch17 TUI Engine | Render cycle cooperates with output-guard |
| Related | Ch13 Extensions | Trust determines whether extensions load |
| Related | Ch12 System Prompt | Trust determines whether .pi/SYSTEM.md applies |
| Related | Ch08 Auth & OAuth | http-dispatcher carries OAuth requests |
Check Your Understanding
- If settings did not support hot-reload, how would the user experience degrade?
- If output-guard flushed at the wrong point in the render cycle, what problems would arise?
- Why is "global extensions loading before trust" safe, but "project extensions loading before trust" dangerous?
Takeaways
- Two-layer settings (global + project) hot-reload — changes take effect immediately, no restart
- Project trust gate: untrusted → no project extensions/skills/SYSTEM.md loaded
- output-guard takes over stdout — subprocess output recruited as TUI content
- Telemetry fully disableable (PI_TELEMETRY=0), only reports install event
This chapter is based on the main-branch source of earendil-works/pi.