← Back to Blog
AI Coding·23 min read

Pi Coding Agent: The SDK Is the Real Reason to Care

Listen to this article

I installed the pi coding agent because my feed wouldn't shut up about it, and I fully expected to uninstall it the same afternoon. That's not cynicism, it's pattern recognition. In 2026 the terminal coding agents have mostly converged. They read files, they write files, they run bash, they stream tokens at you in a nice box. Swap the logo and half of them are indistinguishable.

Day one with pi did nothing to change my mind. It felt sparse to the point of feeling broken.

Then I installed a handful of extensions, and about a week later I noticed something. I'd been splitting my work across Claude Code, OpenCode and Grok Build, and now pi had quietly absorbed most of it. Claude Code is the thing I open when I specifically want a Claude model. Everything else moved. What surprised me more is that the CLI isn't actually the interesting part. The SDK is. Once you can call an agent as a library from a cron job, with only the tools you handed it, you stop thinking about coding assistants and start thinking about small autonomous programs that happen to have a language model in the middle.

This post covers the install, the extensions that saved it for me, an honest account of the security model (it doesn't really have one), and a worked SDK example: a document processing agent that turns messy supplier invoices into validated structured JSON.

What is the pi coding agent?

Pi is a minimal, MIT-licensed terminal coding agent and TypeScript agent toolkit. It ships with four built-in tools (read, write, edit, bash) and a system prompt that, together with those tool definitions, comes in under 1,000 tokens. It supports 15+ model providers with mid-session switching, and everything beyond the core is meant to be an extension you write yourself.

It was created by Mario Zechner, who you might know from libGDX or Spine, and it's now stewarded by Earendil Inc., the company Armin Ronacher co-founded. The repo lives at earendil-works/pi. Star count is somewhere north of 80,000 as I write this, though it's climbing fast enough that whatever number I put here will be wrong by the time you read it.

The design thesis is aggressive minimalism. No MCP. No sub-agents. No plan mode. No built-in todo list. No permission sandbox. Zechner's position is roughly "if I don't need it, it won't be built," and the intended response to a missing feature is that you ask pi to build it for you, then reload.

Quick naming note, because search engines are hopeless at this: this pi is not Inflection's Pi chatbot, not a Raspberry Pi, and not github.com/badlogic/pi, which is a completely different CLI for managing vLLM GPU pods. Zechner has said he picked an un-Googleable name as a joke so nobody would ever use it. That plan failed spectacularly.

Install pi coding agent

npm install -g --ignore-scripts @earendil-works/pi-coding-agent

Or curl -fsSL https://pi.dev/install.sh | sh if you'd rather have the standalone binary. You need Node 20+ or Bun. It runs on macOS, Linux, Windows, and apparently Termux on Android, which I have not tried and probably never will.

Then either /login for subscription OAuth, or drop API keys into your environment or ~/.pi/agent/auth.json.

One trap worth knowing about: the packages moved from the @mariozechner/* scope to @earendil-works/* back in May 2026. Any tutorial or Stack Overflow answer older than that will point you at deprecated packages. pi update --self handles the migration if you were already on the old scope.

Pi coding agent setup

Context files work the way you'd hope. Pi loads AGENTS.md hierarchically, global first then project, and it also reads CLAUDE.md, so migrating an existing repo is genuinely a no-op. Nice touch.

Sessions auto-save to ~/.pi/agent/sessions/ organised by working directory. pi -c continues the most recent one, pi -r gives you a browser, --fork branches. They're trees rather than flat logs, so you can rewind a bad state, go do a side quest, and merge a summary back. Stored as plain JSONL, which means you can grep it, which I appreciate more than I expected to.

The first time you open a project with local .pi/ config, pi asks whether you trust it. Remember that prompt, because it's more limited than it sounds and we'll come back to it.

A week in: what's missing, and the extensions that fixed it

My first two hours with pi were mildly unpleasant. Not because it was bad, but because I'd internalised a bunch of conveniences from Claude Code, OpenCode and Grok Build and didn't realise how much I was leaning on them until they weren't there. No todo list, so no sense of what the agent thinks it's doing across a long task. No sub-agents. Minimal status feedback. It felt like being handed a text editor with no syntax highlighting after five years of an IDE.

Then I read the reasoning, and some of it is actually good. MCP servers burn 13,700 to 18,000 context tokens before you've typed a single character, and progressive disclosure through a CLI plus a README does the same job for almost nothing. Sub-agents are opaque black boxes with lousy context transfer. Built-in plan and todo state confuses models, where a PLAN.md file does the job and survives across sessions and tools.

I buy the MCP one completely. I'm less convinced on todos, which is why the first thing I did was add one.

Here's my current install list, in rough order of how much I'd miss them:

  • @juicesharp/rpiv-ask-user-question. Lets the agent stop and ask me a clarifying question instead of guessing. This is the one that changed the most. Half of a bad agent run is the model committing to the wrong interpretation in the first thirty seconds, and a single question at the right moment saves the whole session.
  • @juicesharp/rpiv-todo. Ironically the exact feature the project deliberately omitted. It's a todo list for the model, rendered as a live overlay, and it survives /reload and compaction. That last part matters more than it sounds.
  • pi-lens. Real-time code feedback: LSP, linters, formatters, type checking. Pi with no language server is a bit like coding in Notepad, and this closes the gap without dragging in a whole IDE.
  • pi-agent-browser. Browser automation through agent-browser rather than Playwright. I've switched to it for basically everything now. It's dramatically more token-efficient than driving Playwright through an agent, which historically meant dumping enormous DOM snapshots into context and watching the model drown.
  • @tintinweb/pi-subagents. Claude Code style sub-agents. I use them narrowly, for fanning out independent read-heavy work, which is the case where the context-transfer objection doesn't really bite.
  • pi-powerline-footer. Purely cosmetic. I regret nothing. Seeing model, thinking level and token spend at a glance genuinely changes how carefully you spend context.

Installing is one line each (pi install npm:pi-lens and so on), and that's the real point. Not any individual extension, but that assembling my ideal setup took an evening, and the extensions API is just TypeScript with hot reload. You can bundle your own into a Pi Package and install it elsewhere from npm or git. When your agent harness is missing something, you don't file an issue and wait.

One caveat that follows directly from that openness: pi packages run with full system access, extensions execute arbitrary code, and skills can instruct the model to run anything. Read the source before you install a stranger's package. I'll come back to this.

Now the part I can't prove. My gut says pi gives me slightly better results than the other CLIs on comparable tasks. My best guess is that it's the tiny system prompt and the absence of clutter, since less junk in the context window means more of the model's attention lands on my actual problem. That lines up with everything I've written about context engineering for AI agents. But it's a feeling, not a measurement, and I'm not going to dress it up as one. Let's look at numbers other people collected instead.

Pi coding agent vs Claude Code

Pi Claude Code
Licence MIT, open source Proprietary
Models 15+ providers, switch mid-session Anthropic only
System prompt Under 1k tokens ~14k tokens
Built-in tools 4 10+
Sandbox / permissions None by default Deny-first plus sandbox
Extensibility TypeScript extensions plus SDK MCP, hooks

Worth knowing that minimal doesn't mean weak: pi placed second on Terminal-Bench running Claude Opus 4.5, beaten only by the benchmark's own minimal harness, without MCP, sub-agents or plan mode. Databricks' harness benchmark from 5 August 2026 is more interesting though. Running the same model at the same thinking effort through different harnesses produced cost differences of more than 2x while quality stayed flat, with simple harnesses like pi often coming out best. Their codebase, their tasks, so it's not a universal law. But "the harness materially changes your bill" is worth sitting with.

Now the part that will actually decide this for most people: money.

On 4 April 2026 Anthropic's billing enforcement landed, and third-party tools started drawing from extra usage instead of subscription limits. Pi's own provider docs still state it plainly: Claude Pro/Max auth works, but third-party harness usage is billed per token, not against your plan. Translation, you can log in, you just don't save any money. There have been partial reversals since, so verify before budgeting around it. Codex, xAI and Copilot logins all work normally and still save you money.

Which brings me to my actual setup, because it's the only honest answer to "is this a Claude Code alternative." I run Grok, ChatGPT/Codex, OpenCode Go and Qwen through pi. Claude stays in Claude Code, where it's still covered by my subscription. The result is that I barely open Claude Code anymore, because most of my agent work now runs on my OpenCode Go subscription with DeepSeek V4 Flash 0731, which is very good and absurdly cheap. The recently released version is significantly better than the preview I tested in my DeepSeek V4 review.

Pi didn't replace Claude Code for me. It made my Claude subscription optional, which is a different and slightly more uncomfortable thing.

Pi coding agent with Ollama and local models

Local models plug in through an OpenAI-compatible entry in models.json, so Ollama, LM Studio, vLLM and llama.cpp all work without ceremony. Set expectations accordingly: on a 24GB M4 MacBook Air they're fine for scoped work like writing an extension or summarising a file, and they fall over on agentic work across a large repo about forty minutes in, when the context window folds. Unless privacy is the entire point, a cheap hosted model is the better trade.

Pi is YOLO by default, and it means it

Two things happened in my first week that I want you to know about before you install this.

First: I was mid-task on a new project, and pi decided it needed examples of what I was trying to build. So it went and searched a bunch of folders across my machine looking for similar code. Not the project directory. My machine. It didn't ask, it didn't announce it, it just did it and then carried on like nothing had happened. To be fair, the examples it found were relevant. That's almost worse.

Second: I built a skill for an image generation CLI I'd written in Go, and pi installed it globally, for every agent, without checking with me first.

Neither of these is a bug. From the README: pi has no built-in permission system for restricting filesystem, process, network, or credential access, and by default it runs with the permissions of the user and process that launched it. Zechner's rationale is that once an agent can write and execute code, permission dialogs are mostly security theatre, and you cannot solve the read-data plus execute-code plus network trifecta without severing the network, at which point you've built a very expensive autocomplete. He's not wrong, exactly. I've made roughly the same argument about --dangerously-skip-permissions, which is the setting everyone turns on by day three anyway. Pi skips the pantomime and starts there.

And remember that project trust prompt? It's an input-loading guard. It stops a repo silently changing your settings and extensions before you approve. It does not restrict what any tool does once you're working, and prompt injection from repo files or command output is explicitly treated as expected local-agent risk.

So: containerise anything you wouldn't personally review. The docs cover three patterns. Gondolin is the nicest, routing tools and shell commands into a local Linux micro-VM while pi and your provider credentials stay on the host. Plain Docker is the simplest, with the caveat that your API keys go into the container. NVIDIA's OpenShell gives you policy-level control and can keep raw keys outside the sandbox entirely. My line is simple: if you're running an unattended loop where you won't read every diff, and recursive agent loops are exactly that, a sandbox isn't optional.

Credit where it's due, though. The supply-chain hygiene is unusually serious for a project this young: exact version pinning, min-release-age=2 to avoid same-day dependency releases, a published shrinkwrap, --ignore-scripts on documented installs, a scheduled audit workflow. The risk here is at runtime, not in the dependency tree.

Building with the pi SDK: a document processing agent

Here's the shift that made pi interesting to me rather than merely pleasant.

Pi runs in four modes. Interactive is the TUI you've been reading about. Print mode (pi -p "...") is single-shot for scripts. RPC mode speaks JSON over stdin and stdout, which is how you'd drive it from Go or Python. And then there's the SDK, which runs the agent in-process inside your own Node program. That last one is the one nobody talks about enough.

It's not a CLI you shell out to. It's a library you call:

import { createAgentSession, ModelRuntime, SessionManager } from "@earendil-works/pi-coding-agent";

const modelRuntime = await ModelRuntime.create();
const { session } = await createAgentSession({
  sessionManager: SessionManager.inMemory(),
  modelRuntime,
});

session.subscribe((event) => {
  if (event.type === "message_update" && event.assistantMessageEvent.type === "text_delta") {
    process.stdout.write(event.assistantMessageEvent.delta);
  }
});

await session.prompt("What files are in the current directory?");

That's a complete, working agent. Nine lines of setup and you have the same loop that powers the CLI, streaming into your process.

The session object is where the useful surface lives. prompt() sends a turn and resolves when the agent settles. steer() interrupts mid-stream to redirect it, which is the programmatic version of typing while it's thinking. followUp() queues a message to be consumed once it's idle. setModel() and setThinkingLevel() change models mid-session, so you can run cheap extraction on a small model and escalate to something bigger only when a document fails validation. compact() summarises history when you're running long. abort() and dispose() do what you'd expect, and you'll want both wired to your job timeout.

SessionManager decides where conversations live. inMemory() for throwaway batch jobs, create(cwd) to persist to disk, open(path) to reload one. Sessions are trees, so branch() lets you fork a conversation, try something, and keep or discard it. One gotcha here that bit me: session replacement (new session, resume, fork, import) lives on AgentSessionRuntime via createAgentSessionRuntime(), not on AgentSession. And because subscriptions attach to a specific session object, you have to re-subscribe after any replacement or your event handler goes quiet with no error.

ModelRuntime owns credential resolution and model catalogues. It reads your existing ~/.pi/agent/auth.json, which means an SDK program inherits whatever you already logged into on the CLI. Convenient for local development, and something to think about carefully before you ship it anywhere.

So what can you actually build with this? Code review bots that comment on PRs. Scheduled maintenance loops that upgrade dependencies overnight. Custom TUIs and web UIs on top of the agent loop. CI steps that triage a failing test before a human looks. Shopify built pi-autoresearch, an autonomous optimisation loop, as a pi extension, and reported unit tests running 300x faster and roughly 20% faster React component mounting off the back of it. The most famous example is OpenClaw, which is built on pi's SDK and is, at last count, the most-starred repository on GitHub by a comical margin.

The SDK docs are solid and there are working examples in the repo ranging from minimal to full control. Pin your versions, though. The API is stabilising but not frozen, and the scope migration already broke a generation of tutorials.

The example I'll build here is document processing, because it's the least glamorous and most universally useful thing a small business has. Drop a supplier invoice into a folder, get validated structured JSON out. It's the kind of boring automation I keep recommending in my writing on AI agents for small business, and it exercises the three things that matter most: custom tools, structured output, and tool restriction.

The document layer, and one myth to kill first

anydoc is Firecrawl's Rust document parser, MIT licensed, and it converts 14 office formats (docx, xlsx, pptx, odt, rtf, epub, csv, pdf and friends) to clean Markdown in under 5 milliseconds per document. No ML models, no external services, entirely local. Detection is content-based rather than extension-based, so a .pdf that's secretly a Word file still converts.

It does not do OCR, and it does not extract to a schema. This is the single most common misconception about it and the entire architecture depends on getting it right. anydoc reads the text layer of a PDF. Hand it a scanned or photographed invoice and you get back ConvertError::Unsupported.

So the design is two-path. anydoc as the fast local path for digital files, and Firecrawl's hosted /v2/parse endpoint with mode: "ocr" as the fallback when anydoc throws. Be clear-eyed about the trade: the moment you hit /parse, your supplier invoices leave the machine. For accounts payable data that's a real decision, not a footnote, and if you want a fully local story you'd pair anydoc with Docling or a local vision model instead. Also worth noting that anydoc launched this month, so edge cases on weird real-world files are going to surface.

Forcing structured JSON out of a pi agent

Pi has no native response-schema option. There's no responseFormat on createAgentSession() or on prompt(). I looked. Don't waste the twenty minutes I did.

The idiomatic pattern is a terminating tool: you define a tool whose parameters are the schema you want back, and whose execute() returns terminate: true.

import { defineTool } from "@earendil-works/pi-coding-agent";
import { Type } from "typebox";

const emitInvoice = defineTool({
  name: "emit_invoice",
  label: "Emit Invoice",
  description: "Return the extracted invoice as your final action.",
  promptGuidelines: [
    "Call emit_invoice exactly once, as your last action.",
    "Do not emit another assistant response after calling it.",
  ],
  parameters: Type.Object({
    supplier: Type.String(),
    abn: Type.String({ description: "11 digit ABN, digits only" }),
    invoiceNumber: Type.String(),
    issueDate: Type.String({ description: "ISO 8601" }),
    subtotal: Type.Number(),
    gst: Type.Number(),
    total: Type.Number(),
  }),
  async execute(_id, params) {
    return {
      content: [{ type: "text", text: `Extracted invoice ${params.invoiceNumber}` }],
      details: params,
      terminate: true,
    };
  },
});

Three things here that cost me time, so let me save you some:

Import from bare typebox, not @sinclair/typebox. The current @earendil-works packages use the bare one. Older forks and tutorials use the scoped one, you'll get confusing type errors, and you'll blame yourself.

terminate only fires when every finalised tool result in the batch is terminating. Pi runs tool calls in parallel by default. If the model calls your emit tool alongside anything else in the same batch, the run keeps going.

content is what the model sees, details is for your code. This split is the nicest small design decision in the whole SDK. Keep the LLM-facing text short and human, stash the machine-readable payload in details, and read it back off the turn_end event's toolResults or the tool_execution_end event's result.

Writing custom tools that do real work

The emit tool above is a shape, not a worker. The tools that actually earn their keep are the boring deterministic ones.

Here's the GST validator. Notice how little of it involves the model:

const validateGst = defineTool({
  name: "validate_gst",
  label: "Validate GST",
  description:
    "Check Australian GST arithmetic on an invoice. Call this before emitting the invoice.",
  parameters: Type.Object({
    subtotal: Type.Number({ description: "Ex-GST amount" }),
    gst: Type.Number(),
    total: Type.Number(),
  }),
  async execute(_id, { subtotal, gst, total }) {
    const problems: string[] = [];
    const round = (n: number) => Math.round(n * 100) / 100;

    if (round(subtotal + gst) !== round(total)) {
      problems.push(`subtotal + GST is ${round(subtotal + gst)}, invoice says ${total}`);
    }
    if (gst !== 0 && round(subtotal * 0.1) !== round(gst)) {
      problems.push(`GST should be ${round(subtotal * 0.1)} at 10%, invoice says ${gst}`);
    }

    return {
      content: [
        {
          type: "text",
          text: problems.length
            ? `GST check failed:\n${problems.join("\n")}`
            : "GST arithmetic checks out.",
        },
      ],
      details: { ok: problems.length === 0, problems },
    };
  },
});

Three design rules I'd defend to anyone:

Never let the model do arithmetic you care about. Give it a function that does the maths and let it reason about the result. The model's job is interpretation and edge cases, not addition. Same goes for the ABN checksum, duplicate detection, and date parsing.

Write the description and promptGuidelines for the model, not for your future self. These fields are the entire interface between your code and the agent's decision making. "Check Australian GST arithmetic, call this before emitting" is a usable instruction. "Validates GST" is not. This is context engineering at the tool level, and it's where most of the tuning time goes.

Return errors by throwing. Returning an error-shaped object never sets the error flag on the result, which is the sort of thing you discover at 11pm. If the tool genuinely failed, throw from execute().

One more constraint worth knowing before you design your schemas: use StringEnum from @earendil-works/pi-ai rather than TypeBox's Type.Union of literals for enum fields. The union form breaks Google's API. It'll work fine on Anthropic and OpenAI and then fall over the day you switch models, which rather defeats the point of a model-agnostic harness.

Give the agent your tools and nothing else

This is where the YOLO problem gets solved properly, and it's my favourite thing about the SDK.

const { session } = await createAgentSession({
  sessionManager: SessionManager.inMemory(),
  modelRuntime: await ModelRuntime.create(),
  noTools: "builtin",
  customTools: [validateGst, verifyAbn, checkDuplicate, emitInvoice],
});

const markdown = await toMarkdown("./intake/invoice-4471.pdf");

let invoice: InvoiceRecord | undefined;
session.subscribe((event) => {
  if (event.type === "tool_execution_end" && event.toolName === "emit_invoice") {
    invoice = event.result.details as InvoiceRecord;
  }
});

await session.prompt(`Extract this supplier invoice. Validate it before emitting.\n\n${markdown}`);

noTools: "builtin" strips read, write, edit and bash entirely. The agent now physically cannot touch your filesystem or run a shell command. It can call four functions you wrote, and that is the complete universe available to it. There's also an allowlist form (tools: ["read", "grep"]) if you'd rather have a read-only agent that can still poke around.

Think about what that buys you. The same harness that wandered off and scanned my home directory unprompted is, in this configuration, incapable of doing so. Not discouraged by a system prompt. Incapable, because the tools don't exist. That's a much stronger guarantee than any permission dialog, and it's the argument for the SDK over the CLI in production: you're not asking an agent to behave, you're defining a box it can't reach outside of.

Then subscribe to the event stream for your audit log. You get message deltas, tool execution start and end, turn boundaries, compaction, retries. Log the lot. When a client asks why the agent flagged their supplier's invoice, you want the answer, and you want it without re-running anything.

Where to stop

The agent produces a validated draft and then it stops. That's the whole design.

The obvious next move is a create_draft_bill tool that posts to Xero with Type: "ACCPAY", Status: "DRAFT" and an idempotency key derived from the supplier ABN plus invoice number, so a network retry can't create a duplicate. That's a separate post, because doing it properly means covering ATO tax invoice requirements, the 47% no-ABN withholding rule, ABN Lookup verification and out-of-band confirmation for bank detail changes. Business email compromise accounted for over three billion US dollars in reported losses in the FBI's 2025 IC3 report, and almost all of it hinges on someone changing bank details in a real invoice thread. An agent must never auto-pay. Not ever.

Same pattern, different tools, is how the e-commerce agents I've been building this month are structured. Narrow tool surface, deterministic validation, a hard stop before anything irreversible.

So should you switch?

Run it as a second agent for a week. Don't rip anything out. Point it at a real repo, use /model to swap providers mid-session, and ask it to build the one workflow feature you miss. If after a week you're reaching for it by default, migrate. If you're not, you've lost an hour.

It's a good fit if you're comfortable in TypeScript, you want to do your own context engineering, and you have non-Claude subscriptions to spend. It's a bad fit if you want safety rails out of the box, or if your entire workflow is built around a Claude subscription you're not willing to restructure.

And it's not universally loved. There are people on Hacker News who tried it and found it didn't finish the job compared to Claude Code or OpenCode. That's a real data point, and the DIY burden is real too. A minimal core means you do the assembly.

Here's where I land. As a CLI, the pi coding agent is a peer. It sits comfortably alongside Claude Code, Codex, Grok Build and OpenCode, and I'd be fine using any of them. The differences are real but small, and my gut feeling about better results is a gut feeling.

As an SDK, it doesn't really have an equivalent. A minimal, model-agnostic, MIT-licensed agent you can embed as a library, restrict to exactly the tools you wrote, and drop into a cron job in about fifty lines is a different category of thing to a coding assistant.

The CLI got me to install it. The SDK is why it's still here.

Thomas Wiegold

AI Solutions Developer & Full-Stack Engineer with 15+ years of experience building custom AI systems, chatbots, and modern web applications. Based in Sydney, Australia.

Ready to Transform Your Business?

Let's discuss how AI solutions and modern web development can help your business grow.

Get in Touch