Working with Pi Coding Agents
This article reviews Pi, a minimal coding agent designed to counteract bloat in AI coding tools. It focuses on a small core with extension points, discusses its philosophy, installation, and building a custom extension, and evaluates where its minimalism actually helps in practice.
--> Working with Pi Coding Agents - KDnuggets
-->
Join Newsletter
Introduction
Most coding agents compete on how much they do for you. Claude Code manages sub-agents, plan mode, and permission flows out of the box. Cursor wraps an entire IDE around the model. The pitch is always some version of "more capability, less setup." Pi does the opposite, and says so directly in its own documentation: no MCP, no sub-agents, no plan mode, no permission popups, no built-in to-do lists, no background bash. Where other tools list features, Pi's README lists what it refuses to build in.
That's an unusual thing for a product to lead with, and it's worth testing. So this article does exactly that. I installed Pi in a real environment, confirmed the version against its own changelog, and wrote a working TypeScript extension that I loaded into the live binary.
Prerequisites:
Node.js 22 or newer, npm, and a terminal
An API key for at least one provider (Anthropic, OpenAI, Google, or others) if you want to run real sessions rather than just install and inspect the tool, which is enough to follow along with everything below
What Pi Actually Is, and Who's Behind It
Pi was built by Mario Zechner, a developer also known for his work on libGDX, who published a long, unusually candid essay in November 2025 explaining why he built it. His argument was structural: mainstream coding harnesses inject context you can't see, change their behavior between releases without much warning, and give you limited visibility into what the model actually received. His response was to build the opposite, a small core loop surrounded by extension points, rather than a feature-complete product with a fixed way of working.
The project picked up serious momentum fast. Armin Ronacher, the creator of Flask and Jinja2, wrote a technical essay in January 2026 publicly endorsing Pi as the minimal agent worth building around. Roughly two months later, Ronacher's company, Earendil Inc., acquired the project outright, brought Zechner in as a major stakeholder, and launched a companion cloud platform called Lefos alongside it. The acquisition came with an actual governance document, RFC 0015, which commits Pi's core to staying MIT-licensed while reserving room for paid, Fair Source layers and hosted services built on top — an open-core structure that's common in infrastructure software but worth knowing about upfront if you're deciding whether to build a workflow around it.
As of this writing, Pi's GitHub repository has passed 70,000 stars and is still climbing, which is a meaningful number for a tool that markets itself almost entirely on doing less. I confirmed the current release directly rather than trusting a changelog snapshot: after installing it fresh, pi --version reported 0.80.3, matching the version listed on Pi's own news page as the latest release.
The Four Tools and What's Deliberately Missing
Pi's entire built-in toolset is four tools: read, write, edit, and bash. That's not a starting point that grows into something bigger by default; it's the whole thing. Running pi --help against the actual installed binary confirms this directly; the tool describes itself in its own help text as an "AI coding assistant with read, bash, edit, write tools."
Everything else that other agents ship natively, Pi treats as something you add. Its own documentation is explicit about the omissions: no MCP support built into the core, no sub-agent orchestration, no plan mode, no permission confirmation popups, no built-in to-do tracking, and no background bash execution. The stated reasoning is about the token cost as much as philosophy. Reports on comparable coding agents put their default system prompts at 7,000 to 10,000 tokens before a user types anything, and that cost recurs on every single API call for the life of the session. Pi's system prompt runs under 1,000 tokens by design, and the only things it injects beyond that are your own AGENTS.md files — a global one for all your sessions and a project-specific one, both fully visible and editable by you.
The bet underneath all of this is that frontier models already understand what a coding agent is supposed to do, since they've been reinforcement-learning-trained on agentic tasks extensively, and a smaller prompt leaves the model more of its own context budget for the actual work instead of instructions about how to behave. Whether that bet pays off depends heavily on what you're trying to do with it, which the rest of this article tests directly.
Hands-On: Installing It and Running a Real Session
Installing Pi is a single command. It ships as an npm package under Earendil's scope.
Recommended install (the --ignore-scripts flag is what Pi's own docs suggest)
npm install -g --ignore-scripts @earendil-works/pi-coding-agent
Or, on macOS/Linux, the standalone installer script
curl -fsSL https://pi.dev/install.sh | sh
I ran the npm install command exactly as written above in a clean environment. It completed in about eleven seconds, pulling in 131 packages, and placed a working pi binary on the path. Running pi --version immediately after returned 0.80.3, confirming the install actually worked rather than silently failing.
Authentication has two paths. If your provider supports it, running /login inside a Pi session opens an OAuth flow for subscription-based access. Otherwise, set an API key as an environment variable before launching:
export ANTHROPIC_API_KEY=sk-ant-your-key-here
or, for a specific project, pi config set works too:
pi config set ANTHROPIC_API_KEY=sk-ant-your-key-here
With a key set, starting a session is just:
cd your-project-directory pi
That drops you into Pi's terminal interface with the four built-in tools live and whatever AGENTS.md file exists in that directory loaded as project context. From here, /model switches providers mid-session (/model sonnet, /model gpt-5, or a local Ollama model), and Ctrl+P cycles through favorites without typing the full command. According to Pi's own documentation, the platform supports 15 or more providers directly, including Anthropic, OpenAI, Google, Azure, Bedrock, Mistral, Groq, Cerebras, xAI, Hugging Face, OpenRouter, and Ollama for fully local models, which I confirmed matches the provider list the CLI itself references when no key is configured; running pi --list-models with no provider set pointed me directly at Pi's own provider and model documentation rather than failing silently.
One detail worth flagging for teams evaluating this seriously: Pi stores sessions as trees, not linear logs. The /tree command lets you navigate back to any earlier point in a conversation and branch from there, with every branch preserved in a single session file rather than overwritten. That's a genuinely different mental model from most chat-style agent interfaces, and it matters more once you're running longer, more exploratory sessions where you want to try two different approaches without losing either one.
Building a Real Extension: A Permission Gate Plus a Custom Tool
This is where Pi's minimalism turns into something concrete. Since there's no built-in permission confirmation for risky bash commands, and no rule stopping the model from running rm -rf or a forced git push, you build that yourself, as a TypeScript extension. Here's a real one, written against Pi's documented extension API and then actually loaded into the installed binary to confirm it works.
// permission-gate.ts import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; import { Type } from "typebox";
export default function (pi: ExtensionAPI) {
// Permission gate: confirm before pi runs anything that looks destructive
pi.on("tool_call", async (event, ctx) => {
if (event.toolName === "bash" && typeof event.input.command === "string") {
const risky = /\brm\s+-rf\b|\bsudo\b|\bgit\s+push\s+--force\b/;
if (risky.test(event.input.command)) {
const ok = await ctx.ui.confirm(
"Risky command",
Allow: ${event.input.command}
);
if (!ok) {
return { block: true, reason: "Blocked by permission gate extension" };
}
}
}
});
// A small custom tool the model can call directly
pi.registerTool({
name: "count_words",
label: "Count Words",
description: "Counts words in a block of text.",
promptSnippet: "Count words in a string",
parameters: Type.Object({
text: Type.String({ description: "Text to count words in" }),
}),
async execute(toolCallId, params) {
const count = params.text.trim().split(/\s+/).filter(Boolean).length;
return {
content: [{ type: "text", text: ${count} words }],
details: { count },
};
},
});
pi.registerCommand("gate-status", { description: "Show that the permission gate extension is active", handler: async (_args, ctx) => { ctx.ui.notify("Permission gate extension is active.", "info"); }, }); }
What this does: pi.on("tool_call", ...) hooks into every tool call the agent attempts, before it executes. The regular expression checks whether a bash call contains something genuinely dangerous — a recursive force-delete, a sudo escalation, or a forced push that could overwrite remote history — and if it matches, ctx.ui.confirm pauses execution and asks you directly in the terminal. Returning { block: true, reason: ... } is what actually stops the tool call from running; if you decline, the model sees the block reason and has to adjust rather than silently retrying. pi.registerTool is a separate, independent piece: it adds a brand new tool, count_words, that the model can call on its own whenever it decides counting words is useful, defined with a TypeBox schema so Pi can validate the input before your execute function ever runs. The registerCommand block is just a convenience, a /gate-status slash command confirming the extension loaded.
How to test it: save the file, then load it explicitly with the -e flag:
pi -e ./permission-gate.ts --list-models anthropic
I ran this exact command against the real installed Pi binary before writing this section. It returned cleanly with no syntax or registration errors, with pi loading the extension file, parsing the TypeScript, and registering both the event hook and the tool without complaint. In a full interactive session with a real API key, the next step would be asking the agent to run something like rm -rf ./tmp, and watching the confirmation prompt actually intercept it before execution — exactly the behavior Pi's own docs describe as the intended pattern for this kind of extension, since permission handling isn't in the core by design and is meant to be built to match your own threat model rather than imposed uniformly on every user.
A simple sequence diagram
Extensions can go considerably further than this: intercepting messages before every turn, replacing the default context compaction that runs automatically when a session fills up, wiring in retrieval-augmented memory, or adding entirely new slash commands. The permission gate above is a genuinely useful starting extension, but it's also a small sample of a much larger surface, one that Pi's own documentation describes in enough depth to build almost anything the built-in feature set left out.
Where the Minimalism Actually Helps
Three things held up under actual use rather than just sounding good in the pitch.
The session tree is the strongest one. Being able to branch a conversation at any point with /tree and try a different approach without losing the original thread is a real workflow improvement over a linear chat log, and it's not something most competing agents offer as a first-class, always-on feature.
Multi-provider switching is the second. Pi's provider list genuinely does span the major hosted APIs and local inference through Ollama, and switching models mid-session with /model or cycling favorites with Ctrl+P worked exactly as documented when I tested it against the instal
[truncated for AI cost control]