Skip to content

The pi harness journey: a coding-agent harness built from one

A coding-agent harness that has been built and maintained with the same agent it constrains, from 2026-01 to 2026-10. This page is the hub of the series: the history, the layout, the extension and skill catalogue, and the lessons that survive the specifics.

TL;DR:

  • The harness started as an opencode configuration (2026-01-19) that grew into a fork of opencode. It moved to pi (Earendil Works) from 2026-05-21, opencode was retired on 2026-08-15, and Claude Code joined as a second harness that month, sharing guard cores from a single stow-linked dotfiles repo.
  • 93 top-level extension files (80 shared library files, 67 of them guard and system cores), 84 skills, and 74 promoted to Claude Code via per-skill symlinks (plus a real lsp-servers plugin directory) - loaded from one source of truth: the dotfiles repo, symlinked into $HOME via GNU stow. A config file is dead until stow links it; stow-drift runs before every claim that config works.
  • The guard stack moved from per-command approval prompts to mechanical gates: a provenance gate on emitted specifics, a block on AI prose tells, a confidential-identifier block with user confirmations, a tiered git gate that lets routine git run unprompted (4 denials in 3,833 prompts over 30 days), and a pre-push leak gate on the bytes that leave the machine.
  • The tool-routing rules - which tools to use for what, when to stop searching, how to escalate - are a single prepended file read every turn. At about 40 KB, it governs tool choice on every request.
  • The 2026-10-19 package survey examined ~5,300 community pi packages. Verdict: install one trial (prefix-cache compaction); every other package either duplicated a subsystem the harness already owned or carried a larger surface than the idea worth taking.

The dotfiles repo gained an opencode configuration on 2026-01-19 (ae30617), with MCP servers for Cloudflare docs, mermaid, TypeScript and fetch. Over the following months that grew into a fork of opencode itself, carrying its own patches and a FORK.md describing them (methodology injection among them), kept in sync with upstream by hand.

The move to pi (2026-05-21) and opencode’s retirement (2026-08-15)1

Section titled “The move to pi (2026-05-21) and opencode’s retirement (2026-08-15)1”

The migration to pi started on 2026-05-21: a converter that moved opencode’s SQLite sessions into pi’s JSONL format (1641264), the first pi guards (git-gh-gate.ts, 66067f6), the web research tool (a6b716d), and the docs and context7 integrations that closed the remaining gaps (a670b70). From then on pi was the harness in daily use and opencode the fallback.

On 2026-08-15 opencode was retired (c4b39c9): the .config/opencode/ tree was deleted from the dotfiles repo (git history is the archive) and the tool-routing rules moved to .pi/agent/prompts/tool-routing.md, canonical since that day and prepended to the system prompt by tool-routing.ts on every turn.

Claude Code joined as a second harness in the same month, sharing the guard cores in .pi/agent/extensions/lib/. The port document (pi-to-claude-code-port.md, 17 commits through 2026-08-12) maps pi’s in-process TypeScript extensions to Claude Code’s separate mechanisms: MCP servers for tools, hook scripts for guards, a statusline for TUI state, commands for slash prompts, and subagents for delegation.

Guards (2026-05 to 2026-10). The earliest pi extensions were guards: git-gh-gate.ts (2026-05-21) and tool-guard.ts (2026-05-22). confidential-write-guard.ts (2026-06-26) blocks user-confirmed confidential identifiers. epistemic-guard.ts (2026-08-03) is the provenance guard: it blocks an emitted version, flag, path, URL, or CVE id with no source in the current session’s tool results. ascii-punctuation-guard.ts and ai-tell-guard.ts enforced prose rules mechanically. By 2026-10-20, 18 extensions hooked tool_call, and the command/git safety work replaced per-command approval prompts with a tiered git gate: the prompts had been denied 4 times in 3,833 over 30 days, so routine git now runs unprompted while leak protection moved into a pre-push gate and a Forgejo server-side pre-receive hook.

Memory store (2026-08 to 2026-10). memledger.ts (2026-08-09) connected pi to a Postgres-backed cross-client session store with full-text and vector search, seeded from 640,000 existing messages2. memledger-seed.ts (2026-08-16) added event-driven live ingest. By 2026-10 the store served seven search tools (memledger_search, search_messages, semantic_search, search_ledger, search_memories, get_memory, list_sessions) and was the only full copy of sessions older than 30 days (local FTS5 logs prune on that schedule).

Context and compaction (2026-08 to 2026-10). tool-output-spill.ts (2026-08-21) and tool-output-prune.ts (May 2026, algorithm ported from the opencode fork) managed oversized tool output. The prefix-stable pruning rewrite (2026-10-10) used hysteresis so the prune only fires past a threshold, keeps the pruned set monotone, persists decisions as context_edit entries, and works on a structuredClone so sub-floor calls leave the prefix byte-identical for prompt caches. The prefix-compaction.ts trial (2026-10-20) tested a compaction that re-sends the last request with one summarise instruction appended, so the provider’s prefix cache could serve the history. It was slower than pi’s default on both providers in use and ships off by default; the context and prompt-cache reference has the numbers.

Loops (2026-07-13 to 2026-10). The self-correcting loop shipped incrementally: a sensor-gated inner loop with a governor (wall-clock budgets, cgroup limits, git checkpoint/rollback), model-escalation ladder, and adversarial review. loop graph shipped 2026-10-06 (commit fa5e895) and added dependency-graph scheduling for multi-loop runs. garden (2026-10-13) runs an unattended skills-lint and stow-drift check every Sunday at 06:00.

Research stack (2026-05 to 2026-10). A self-hosted web research stack grew alongside: SearXNG metasearch, a multi-tiered crawler (curl_cffi to Playwright), OSINT tools, a Singapore rental due-diligence suite, and MCP integration. The stack answered the “Exa returns 0 results on a long-tail local query, now what” problem with a bidirectional escalation rule (SearXNG silent empty -> Exa, and vice versa).

The 2026-10 package survey. On 2026-10-19, the pi.dev package registry listed ~5,300 community packages. A survey of the top 300 by downloads plus 16 that overlapped the harness’s own extensions found exactly one trial worth installing (prefix-cache compaction). Every other candidate either duplicated a subsystem the harness already owned (subagents, todos, guards, LSP, memory) or carried a much larger surface than the idea worth taking. Every package was read via npm pack; nothing was installed or executed.

Command and git safety (2026-10-20). The tiered git gate replaced per-command approval prompts. Routine local/recoverable git (commit, push without force/delete, worktree add/remove, merge, switch) runs unprompted when global hooks are active, logged to an audit trail. History rewrites, force/delete pushes, and hook bypasses still prompt. Leak protection moved into a client pre-push gate (gitleaks, blocked terms, sensitive files, and on public targets an identifier/provenance scan plus a local-model review) and a Forgejo server pre-receive hook. A daily visibility cache tells every layer which repos are public. The agent-inventory tool reports leftover worktrees, branches, unpushed commits, stashes, and dirty trees across repos.


The source of truth is a single dotfiles repo symlinked into $HOME via GNU stow. The rule that governs everything: a config file is dead until stow-linked. stow-drift exits 1 on any live-tree file with no symlink, and is run before every claim that config works.

dotfiles repo (source of truth)$HOME (stow symlinks).pi/agent/extensions/93 top-level filespi harnessprimary; all extensionsstow.pi/agent/extensions/lib/80 files (67 cores).claude/hooks/ + mcp/toolkit.tsskills/ -> 74 symlinks+ lsp-servers pluginimported by CC hooks.config/git/hooks/pre-push leak gateimported by git hooks.pi/agent/skills/84 skillsClaude Codesecond harness; curated subsetper-skill symlinks.pi/agent/prompts/tool-routing.mdAPPEND_SYSTEM.mdlocal-model-rules.mdtool-routing prepended every turn.pi/agent/mcp.jsonnative MCP servers
  1. The dotfiles repo holds everything: extensions, shared library cores, skills, prompt fragments, MCP server config, Claude Code adapters, and git hooks.
  2. GNU stow symlinks the relevant files into $HOME. stow --no-folding on a fresh machine; the repo’s .stow-local-ignore excludes what should not be linked (.git, README.md, AGENTS.md, package.json).
  3. pi loads all top-level .pi/agent/extensions/*.ts files (and */index.ts) at startup. Claude Code reads its own .claude/hooks/ and .claude/mcp/toolkit.ts, which import shared cores from ../../.pi/agent/extensions/lib/. Git hooks import the same cores.
  4. stow-drift catches the failure mode where a repo file has no live symlink. Exit 1 means dead config.

A pi extension registers in one TypeScript file: it can define a tool, intercept tool calls, inject context, and paint the TUI. Claude Code splits the same surface across five mechanisms: MCP servers (tools), hook scripts (guards), statusline (TUI state), commands (slash prompts), and subagents. The shared-core pattern (lib/<name>-core.ts) keeps the logic in one file, with thin pi adapters and Claude Code adapter scripts that import it. Git hooks import the same cores for the pre-push gate. When a guard rule changes, it changes once.

The shared library: 80 files as of 2026-10-20 (67 of them -core.ts shared cores), covering every guard (epistemic, confidential-write, ascii, ai-tell, secret-output, dangerous-cmd, untrusted-content), the git gate and pre-push scan, tool-output spill/prune, context inspection, the agent view, worker inbox, worker session persistence, skill-guard, task limits, and more.

Two prose files shape every turn:

  • tool-routing.md (prepended by tool-routing.ts): the search-family pipeline, web research escalation, docs.erfi.io workflow, LSP-for-code-intel rule, subagent delegation policy, memory and session-search triggers, bash discipline, and the “bg-tasks family” rules. It resolves self-relative to the extension’s module path so pi-package checkouts loaded in place work too.

  • APPEND_SYSTEM.md (appended every turn): commit/PR authorship rules (no AI-attribution trailers, no Co-Authored-By), safety rules, epistemic calibration, confidential identifiers in tracked files, output surface formatting, and AI prose tells.

Both are hand-synced to .claude/CLAUDE.md for Claude Code. A local-model-rules.md fragment is injected only when a local llama-server model is in use.

84 skills in .pi/agent/skills/, loaded by pi on startup. They are markdown files with frontmatter: a description (the trigger text the model sees, capped at 500 characters) and a body (capped at 500 lines). A lint script (scripts/skills-lint.py) checks frontmatter shape, ASCII punctuation, dead paths, retired terms, and freshness (a metadata.verified date older than 90 days warns). The lint runs as a pre-commit hook and weekly via the garden loop.

74 skills are promoted to Claude Code via per-skill symlinks in .claude/skills/. The directory contents are the allowlist. Private-corpus skills (anything touching personal session data) stay pi-only.

Since 2026-09-29, pi loads MCP servers from a native .pi/agent/mcp.json (previously a separate pi-mcp-bridge git package). Servers support command (local subprocess), url (remote HTTP), and OAuth. Secrets go in ${VAR} env refs or !command substitution, never literal values. Rarely-used servers use "exposure": "deferred" and the model finds them via tool_search.

Project-scoped MCP lives in <repo>/.pi/mcp.json for trusted repos. A global server one project needs sits in the global file with "enabled": false, and that project carries a bare {"enabled": true} override.

Since pi 0.79.0, project-local inputs (.pi/ and AGENTS.md in the working directory) are gated behind a trust decision. Global config (.pi/agent/) loads on every startup with no gate. Trust decisions persist to ~/.pi/agent/trust.json (runtime state, not tracked). Subagent spawners (task.ts, bg-tasks.ts) pass --approve only when the parent trusts the working directory, so a subagent never loads project inputs the user declined.


93 top-level extension files as of 2026-10-20. Grouped by area; the spokes linked from each group dive deeper into the relevant subsystems.

Docs, search, and fetch (docs.ts over SSH, exa.ts, webfetch.ts, web-research.ts, oci-tags.ts, context7.ts), code intelligence (lsp/, glob.ts, grep.ts), session history (session-search.ts, session-ledger/, memledger.ts), data extraction (pdf.ts, opendata.ts, osint.ts, sg-rent.ts, video-review.ts), testing (bench.ts, go-test.ts, hurl-test.ts, osv-scan.ts, secret-scan.ts), subagents and background work (task.ts, bg-tasks.ts, memory.ts, todowrite.ts), rendering (render-diagram.ts, build-favicon-set.ts), and utilities (write-stream.ts, apply-patch.ts, yank.ts, powershell.ts, pg-analyser.pi.ts, bash-error-hints.ts).

Spoke: self-correcting-agent-loop, self-hosted-research-stack, agent-memory-store.

Tool-call interceptors that block, mask, or warn. tool-guard.ts (~30 rules covering bash/write anti-patterns), git-gh-gate.ts (tiered git gate), epistemic-guard.ts (provenance gate for unverified specifics), confidential-write-guard.ts (user-confirmed identifier blocks), ascii-punctuation-guard.ts (smart-punctuation blocker), ai-tell-guard.ts (AI prose-tell blocker), dangerous-cmd-guard.ts (destructive shell command classification), secret-output-guard.ts (registered-secret masking), untrusted-content.ts (prompt-injection defence for web content), skill-guard.ts (active skill routing), plus cd-agents-reload.ts, lookup-before-ask.ts, entity-qualifier-nudge.ts, slash-typo-guard.ts, repeat-tool-guard.ts, tool-output-spill.ts, degenerate-stream-guard.ts, runaway-turn-guard.ts, cost-guard.ts, and git-session-report.ts.

Spokes: agent-guard-extensions, agent-secret-guard, agent-git-without-prompts, agent-leak-gates.

tool-routing.ts (prepends the tool-routing rules), local-model-rules.ts (per-model rules for local engines), style-toggle.ts (output-style switcher), history-first.ts (nudge to search prior sessions before research), skill-first.ts (nudge to open the matching skill), veans-prime.ts (Vikunja workflow priming), local-provider-hints.ts, llmc-dynamic.ts (syncs local-model list), llmc-session-guard.ts (auto-locks the session preset).

agent-view.ts (Claude-Code-style agent list under the editor), context-inspector.ts (/context with prompt-cache break analysis), btw.ts (/btw side-question worker), interactive-shell.ts (real-TTY ! commands), session-fts/ (background FTS5 indexer), session-auto-title.ts, session-summary.ts, session-undo.ts, plus tool-output-prune.ts (prefix-stable), prefix-compaction.ts (provider-side, off by default), compaction-progress.ts, resume-after-compact.ts, continue-after-error.ts, trigger-compact.ts, clipboard-image-shrink.ts, tool-activity.ts, streaming-thinking-guard.ts, transcript-timestamps.ts, vim-editor.ts, llm-metrics.ts, config-snapshot.ts, memledger-seed.ts, atuin.ts, notify.ts, and the disabled custom-footer.ts (superseded by pi’s built-in footer).

Spoke: agent-context-and-prompt-cache, agent-subagents-and-worktrees.

Two extensions are .disabled: custom-footer.ts (superseded by pi’s built-in footer) and stuck-state-recovery.ts.


Each carries the incident or measurement behind it.

On 2026-08-27, a guard extension was written, unit-tested, committed, and documented - but never stow-linked. The guard could not load at session start because no symlink existed from ~/.pi/agent/extensions/ to the repo file. The extension was dead config for the entire session it was meant to protect.

stow-drift now exits 1 on any live-tree file with no symlink, and is run before every claim that config works. Extensions load at session start, so verification happens out-of-band: timeout -s KILL 120 pi -p --no-mcp '<text that should trip the guard>' </dev/null.

Prompts approved 99.9% of the time protect nothing

Section titled “Prompts approved 99.9% of the time protect nothing”

Over 30 days of operation, the git approval prompt was denied 4 times in 3,833 prompts. Four denials across a month, against the cost of stopping the session for every commit, push, worktree add, and branch switch. Two of those four denials were misclicks.

The 2026-10-20 command/git safety work replaced per-command prompts with a tiered gate: routine local/recoverable git runs unprompted when global hooks are active, and leak protection moved into mechanical gates (client pre-push, server pre-receive) the agent cannot skip. The approval prompt remains only for irreversible or remote-config actions.

Guards that block whole compound commands cost more than they catch

Section titled “Guards that block whole compound commands cost more than they catch”

A guard that blocks a compound command (git add -A && git commit -m ...) forces the agent to re-run the entire sequence verbatim after the user approves. The block teaches nothing - the same command runs anyway. Worse, a block on commit prevents the agent from recording state before a risky operation that follows.

The tiered gate’s design split compound commands: let the routine parts through, and only prompt on the dangerous suffix. The dangerous-cmd-guard.ts learned the same lesson independently: classify, do not blanket-block.

The /context command (added via context-inspector.ts) showed that the system prompt was not cache-stable across user prompts: six extensions returned a full systemPrompt from before_agent_start, and any change in that string misses the prefix cache because the system prompt is position 0. session-ledger’s semantic block was one-shot (injected on the first substantive prompt, absent from the second), so the leading system prompt changed once per session. A memory save mid-session changed memory.ts’s block the same way.

The measurement found the problem. The prefix-compaction trial (2026-10-20) was the fix: re-send the last captured request byte-for-byte so the server’s prefix cache covers the whole history. Without /context the problem would have been invisible - the model felt slow, but the mechanism was invisible.

Third-party packages as idea sources, not installs

Section titled “Third-party packages as idea sources, not installs”

The 2026-10-19 survey of ~5,300 community pi packages found one trial worth installing. The rest either duplicated subsystems the harness already owned (subagents, todos, guards, LSP, memory) or carried a larger surface than the idea worth taking from them. The survey used npm pack to read source without installing or executing anything.

The pattern generalises: when a package ecosystem matures, the catalogue is mostly overlapping implementations of the same few subsystems. Read the top candidates for technique, steal the one idea that actually improves on what you have, and skip the rest.

The tool-routing rules govern tool choice on every request

Section titled “The tool-routing rules govern tool choice on every request”

tool-routing.md is about 40 KB of prose read every turn by every model. The rules that changed behaviour the most: “after 2 search calls with no drill-in, open the most likely hit” (stopped the reformulation spiral), “search before asking anyone” (stopped the “what is X?” question to the user when memledger_search held the answer), and “never bash curl a search engine” (stopped the fallback to raw curl when Exa returned nothing - the research stack’s SearXNG and crawler were sitting there the whole time).

Prose rules that constrain tool selection are the cheapest correctness investment per byte. A one-sentence rule (“stop after 2 searches and open a hit”) changes behaviour more than a 200-line guard extension.


  • Guard extensions that enforce prose and process rules - the six core guard extensions and the stow-link incident
  • Keeping credentials out of agent transcripts - the known-value secret guard design
  • Agent git without prompts (spoke, forthcoming) - why approval prompts were replaced by gates
  • Agent leak gates (spoke, forthcoming) - building the pre-push and server pre-receive leak gates
  • Agent context and prompt cache (spoke, forthcoming) - context size, cache stability, compaction trials
  • Agent subagents and worktrees (spoke, forthcoming) - task/bg workers, agent view, worktrees, main-session git
  1. The retirement is recorded in the dotfiles repo’s AGENTS.md (“opencode (legacy TUI, retired)”) and the commit c4b39c9 (2026-08-15): “refactor: retire opencode - two harnesses (pi primary, CC work)”. The tool-routing.md canonical date is from the same commit batch (05e6b20, 2026-08-15). ↩

  2. The 640k figure is from the agent memory store reference. The seeding run and embedding process are described there. ↩