A deep dive into Pi

By

Pi is a minimal open source coding agent for the terminal. Install it, pick a model, extend it with TypeScript, and compare it with Claude Code and Codex.

~~~

Pi is a small, open source coding agent that runs in your terminal. You give it a task, and the model works on your project with four tools: read, write, edit and bash. Everything else, from subagents to permission prompts, is something you add yourself with a TypeScript extension or a package.

Mario Zechner started it, and it’s MIT licensed. In April 2026 he joined Earendil, the company founded by Armin Ronacher and Colin Daymond Hanna, and brought Pi with him. The code now lives at github.com/earendil-works/pi, and the website is pi.dev.

The pi.dev homepage, with the install command

Pi ships new versions every few days, so some details here will move. Everything in this post was checked in September 2026.

Why Pi?

Most coding agents try to do everything for you. Claude Code, Codex and OpenCode all come with plan modes, subagents, MCP clients, todo lists and permission dialogs.

Pi leaves all of that out on purpose. The README has a list of what it doesn’t do:

  • No MCP. Write a CLI tool with a README and let the model call it, or add MCP with an extension.
  • No subagents. Spawn more Pi instances in tmux, or build your own.
  • No permission popups. Run it in a container, or write the confirmation flow you want.
  • No plan mode. Write the plan to a file.
  • No built-in todos, because they confuse models. Use a TODO.md file.
  • No background bash. Use tmux.

What you get instead is a short system prompt, a model picker that covers most providers, sessions you can branch like a tree, and an extension API that reaches into almost everything.

The idea is that you shape the agent around how you work. And the agent can shape itself: ask Pi to write an extension, type /reload, and the new feature is there.

The pi.dev homepage shows this with a live demo, where you ask Pi to change the website you’re looking at.

The "Why Pi?" section on pi.dev, with the interactive Pi demo

This is also why Pi shows up inside other projects. OpenClaw uses it as its agent, and the Flue framework runs on it. The coding agent is one package in a monorepo that also contains pi-ai (one API for many LLM providers), pi-agent-core (the agent loop) and pi-tui (the terminal UI).

Install it

Pi is a Node.js program, so you need a recent version of Node installed.

The simplest way is npm:

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

The --ignore-scripts flag stops dependency install scripts from running. Pi doesn’t need them, and the project recommends it in every install command.

Alternatively, the official installer checks your setup, offers to install Node if it’s missing, and then installs the same npm package:

curl -fsSL https://pi.dev/install.sh | sh

On Omarchy, install Node and npm with omarchy pkg add nodejs npm, then use the npm command. Pi also has a PowerShell installer for Windows.

Check it worked:

pi --version

If you installed Pi before May 2026, you have the old package. It was published as @mariozechner/pi-coding-agent, that name is now deprecated, and it gets no new releases. To get current versions, remove it and install the new one:

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

Your settings, logins and sessions live in ~/.pi/agent, and they stay where they are.

Pick a model

Pi works with a model you bring yourself, and you have three options.

Use a subscription you already pay for

Start pi and type:

/login

Then pick a provider. Pi can sign in with a ChatGPT Plus or Pro plan (through Codex), GitHub Copilot, a Grok subscription, Meta’s Muse, OpenRouter and Claude Pro or Max.

The /login provider list in Pi, with OpenAI Codex already stored

The ChatGPT one is the one I picked. OpenAI officially supports using it from Pi, and requests count against your plan.

Be careful with the Claude one. Pi’s docs say that Claude Pro and Max usage from a third-party agent is billed per token as extra usage, and doesn’t come out of your plan limits. You’ll see it on your bill.

Tokens are saved in ~/.pi/agent/auth.json and refresh by themselves.

Use an API key

Every provider has an environment variable. Export it and start Pi:

export ANTHROPIC_API_KEY=sk-ant-...
pi

The list is long: Anthropic, OpenAI, Google Gemini and Vertex, Amazon Bedrock, Azure OpenAI, Mistral, Groq, Cerebras, xAI, DeepSeek, OpenRouter, Vercel AI Gateway, Cloudflare Workers AI and AI Gateway, Hugging Face, Fireworks, Together AI and more. Run pi --help to see every variable name.

Use a local model

Pi talks to anything that speaks the OpenAI, Anthropic or Google APIs. That includes Ollama, LM Studio and vLLM running on your own machine.

You add them in ~/.pi/agent/models.json. This is the one I have for LM Studio:

{
  "providers": {
    "lmstudio": {
      "baseUrl": "http://localhost:1234/v1",
      "api": "openai-completions",
      "apiKey": "lmstudio",
      "models": [{ "id": "google/gemma-4-26b-a4b" }]
    }
  }
}

LM Studio ignores the API key, but Pi hides models that have no key, so any value works.

Check that Pi sees it:

pi --list-models lmstudio
provider  model                   context  max-out  thinking  images
lmstudio  google/gemma-4-26b-a4b  128K     16.4K    no        no

For Ollama, set baseUrl to http://localhost:11434/v1. Some local servers don’t understand the developer role that Pi sends to reasoning models, and for those you add "compat": { "supportsDeveloperRole": false } to the provider. If you’re new to local models, start with how to run a local LLM with Ollama, or the free Local AI Models course.

Keep your expectations honest here. With Gemma 4 26B, Pi answered questions about a small project correctly, reading the files it needed. When I asked it to write a test file, the model described the tool calls in its thinking instead of making them, and no file appeared on disk. A local model has to be good at tool calling, and that’s where small models fall behind the hosted ones.

A first session

Go to a project and start Pi:

cd ~/www/flaviocopes.com
pi

The screen has a header that lists the AGENTS.md files, skills and extensions Pi loaded. Below that come the messages, then the editor where you type, then a footer.

Type a request and press Enter:

What does this app do? Don't change any files.

The model reads files and runs commands, and you see each tool call as it happens. Ctrl+O folds and unfolds long tool output.

This is what I got when I asked about the repository of this site:

Pi explaining the flaviocopes.com repository, with the footer showing tokens, cost, context and model

The footer is worth a look. It shows the folder and Git branch, the input and output tokens ( and ), the cache reads (R) and the cache hit rate (CH), the cost, and how much of the context window is used. (sub) means the cost is covered by a subscription, in this case my ChatGPT plan. On the right, the model and the thinking level.

A few things make the editor faster:

  • Type @ to fuzzy-search a file and attach it to the message.
  • Start a line with ! to run a shell command and send its output to the model. !! runs it without sending the output.
  • Ctrl+V pastes an image, or you can drag one onto the terminal.
  • Shift+Enter adds a new line, and Ctrl+G opens your $EDITOR for long prompts.

Steer it while it works

You don’t have to wait for the agent to finish. Type while it’s working and press Enter, and Pi delivers that message as soon as the current tool calls finish. It’s how you say “no, use the existing helper” before it writes three more files.

Alt+Enter queues a follow-up instead, which waits until the agent is done. Escape stops everything and puts your queued messages back in the editor.

Switch models and thinking

Ctrl+L opens the model picker, and you can switch in the middle of a session. Shift+Tab cycles the thinking level, from off to max.

If you use two or three models regularly, run /scoped-models to pick them, then Ctrl+P cycles through that short list.

The commands you’ll use most are /model, /settings, /resume, /new, /compact and /tree. /hotkeys shows every shortcut.

Give it project instructions

When it starts, Pi reads AGENTS.md from ~/.pi/agent/, from every parent folder and from the current folder, and joins them together. If there’s no AGENTS.md, it reads CLAUDE.md, so projects set up for Claude Code work as they are.

This is where you put your conventions, the commands to run tests and the things the agent should never touch. I explain what goes in there in the AGENTS.md post.

Pi also lets you change the system prompt itself. A .pi/SYSTEM.md in the project replaces the default prompt, and an APPEND_SYSTEM.md adds to it. The default prompt is short, so you have room for your own.

Sessions are trees

Pi saves every session as a JSONL file in ~/.pi/agent/sessions/, grouped by folder. Resume the latest one with:

pi -c

Or pick an older one:

pi -r

What’s different from other agents is that a session is a tree, not a line. Each entry points to its parent, so you can go back to any earlier message and continue from there. The old branch stays in the file.

Type /tree (or press Escape twice) to see it. Pick a point, and you continue from there. Press Shift+L on an entry to label it as a bookmark.

The other commands build on the same idea:

CommandNew session?Starts fromUse it when
/newYesAn empty conversationThe old context isn’t useful anymore
/compactNoThe current pointThe context is useful but too big
/treeNoAny point in this sessionYou want to try again from earlier
/forkYesAn earlier message of yoursYou want to rewrite an old request
/cloneYesThe current pointYou want a copy before an experiment

Now the thing that confused me. These commands move the conversation, not your files.

I was building a small 3D game with Pi, dogs pulling a sled through the snow. I ran /clone before asking for a change to how the dogs turn, so I could go back if I didn’t like it. Then I asked Pi how to roll back. Going back to the clone restores the conversation, but not the code, because Pi had already written the changes to disk.

To undo files, use Git. Commit before you try something, and git restore . if you don’t like it. Pi ships an example extension, git-checkpoint.ts, that stashes your changes at every turn and restores them when you move in the tree, if you want the two tied together.

A few more session tools:

  • /compact summarizes old messages to free up context. Pi also does it automatically when the context gets close to the limit. The full history stays in the file.
  • /export saves the session as an HTML page.
  • /share uploads it to a private GitHub gist and gives you a link that renders it.
  • --no-session runs Pi without saving anything.

Skills and prompt templates

A skill is a folder with a SKILL.md file that tells the model how to do one job. Pi follows the Agent Skills standard, so the same skills work in Claude Code, Codex and Cursor. If you haven’t written one, the free AI Agent Skills course walks through it.

Pi looks for skills in ~/.pi/agent/skills/, ~/.agents/skills/, and in .pi/skills/ or .agents/skills/ inside the project. It lists the available skills in the prompt, and the model loads the full file only when it needs it. You can also call one directly:

/skill:wrangler

This surprised me the first time. I started Pi in my home folder, and it listed the remotion-best-practices and wrangler skills in the header, because they were already in ~/.agents/skills/ from another agent.

A prompt template is a Markdown file you expand with a slash command. Save this as ~/.pi/agent/prompts/review.md:

---
description: Review staged git changes
---
Review the staged changes (`git diff --cached`). Look for bugs,
missing error handling and anything that breaks the tests.

Now /review in the editor expands to that prompt. Templates take arguments too, with $1, $2 and $@.

Extensions

Extensions are where Pi becomes yours. An extension is a TypeScript file that exports one function. That function gets the Pi API, and with it you can:

  • register tools the model can call
  • add slash commands and keyboard shortcuts
  • listen to events, and block or change tool calls before they run
  • ask the user something with a dialog
  • replace parts of the UI, like the footer or the editor

Pi loads TypeScript files directly, so there’s no build step.

Block a command

Pi has no permission prompts, so let’s add one where it matters. This extension stops the agent from running git push. When Pi runs with a UI, it asks you first. In a script, it blocks the push.

Save it as .pi/extensions/no-push.ts in your project:

import type { ExtensionAPI } from '@earendil-works/pi-coding-agent'
import { isToolCallEventType } from '@earendil-works/pi-coding-agent'

export default function (pi: ExtensionAPI) {
  pi.on('tool_call', async (event, ctx) => {
    if (!isToolCallEventType('bash', event)) return
    if (!/\bgit\s+push\b/.test(event.input.command)) return

    if (!ctx.hasUI) {
      return { block: true, reason: 'git push is blocked, ask the user to push' }
    }

    const ok = await ctx.ui.confirm('git push', `Run "${event.input.command}"?`)
    if (!ok) return { block: true, reason: 'The user said no' }
  })
}

The tool_call event fires before every tool runs. isToolCallEventType narrows it to the bash tool, so event.input.command is typed. Returning { block: true } cancels the call, and the model receives the reason as the tool result.

I asked Pi to run git push origin main in a script, and the bash tool came back with git push is blocked, ask the user to push.

Add a tool

Now a custom tool. I check how many words of prose a post has, without counting the code blocks. Here’s that as a tool, in .pi/extensions/prose-words.ts:

import { readFile } from 'node:fs/promises'
import type { ExtensionAPI } from '@earendil-works/pi-coding-agent'
import { Type } from 'typebox'

export default function (pi: ExtensionAPI) {
  pi.registerTool({
    name: 'prose_words',
    label: 'Prose words',
    description: 'Count the words in a Markdown file, skipping code blocks',
    parameters: Type.Object({
      path: Type.String({ description: 'Path to the Markdown file' }),
    }),
    async execute(toolCallId, params) {
      const text = await readFile(params.path, 'utf8')
      const prose = text.replace(/```[\s\S]*?```/g, '')
      const words = prose.split(/\s+/).filter(Boolean).length
      return {
        content: [{ type: 'text', text: `${params.path} has ${words} words of prose` }],
        details: { words },
      }
    },
  })
}

The description is what the model reads to decide when to use the tool, so write it for the model. parameters is a TypeBox schema, which Pi turns into the JSON schema the model sees. Whatever you put in content goes back to the model.

Ask “How many words of prose are in README.md?” and the model calls prose_words instead of counting by hand.

Where extensions live

Extensions in ~/.pi/agent/extensions/ load in every project. Extensions in .pi/extensions/ load only in that project, and only once you trust it (more on that in a minute). For a quick test, load a file directly:

pi -e ./no-push.ts

Edit an extension and type /reload, and Pi picks up the change without a restart.

Remember you can ask Pi to write extensions for you. Its docs are part of the npm package, and the agent knows where to find them. Pi also ships more than 50 example extensions, including a plan mode, subagents, a permission gate, protected paths, SSH execution, a todo list and, yes, Doom.

Project trust

A cloned repository could ship a .pi/extensions folder, and an extension runs with your full permissions. So the first time you start Pi in a folder that has project settings, extensions, skills or prompts, it asks whether you trust it. The answer is saved in ~/.pi/agent/trust.json, and /trust changes it later.

Scripts don’t get the prompt. In print mode, Pi skips project extensions unless you pass --approve (or -a). I tripped on this with the prose_words tool. Without -a the tool didn’t exist, and the model counted the words on its own. With -a it called the tool.

Note that AGENTS.md and CLAUDE.md load either way. Trust only guards the files that can run code or change settings.

Pi packages

A package bundles extensions, skills, prompt templates and themes, so you can share them through npm or Git:

pi install npm:@foo/pi-tools
pi install git:github.com/badlogic/pi-doom

Add -l to install into the project instead of globally. pi list shows what’s installed, pi config turns single resources on and off, and pi update --all updates Pi and every package.

Creating a package means adding a pi key to package.json that points to your folders. You’ll find packages on npm under the pi-package keyword.

Be careful with packages. Extensions run arbitrary code, and a skill can tell the model to run anything. Read the source before you install, and pin the version you reviewed by adding @ and a version or Git tag after the source.

Use Pi from scripts

You can also run Pi without the interactive screen. -p (print mode) runs one request and prints the answer:

pi -p "What does this project do? Answer in two sentences."

You can pipe text in, and Pi adds it to the prompt:

git diff | pi -p "Write a commit message for this diff"

--tools limits what the model can use. This gives it read-only access, which is what I want for a review in a script:

pi --tools read,grep,find,ls -p "Review the code in src/"

--mode json prints every event as one JSON object per line: the session, each turn, every streaming update, every tool call and its result. It’s useful when another program needs to follow what the agent does, like a CI job that stores the tool calls.

For programs that aren’t written in Node, --mode rpc keeps Pi running and speaks a JSON protocol over stdin and stdout. The docs have one warning about it: split records on \n only. Node’s readline also splits on Unicode line separators that can appear inside JSON strings.

Embed Pi in your app

The npm package is also an SDK. Install it in a project:

npm install @earendil-works/pi-coding-agent

This script creates a session with the LM Studio model from earlier, gives it two read-only tools and streams the answer:

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

const modelRuntime = await ModelRuntime.create()
const model = modelRuntime.getModel('lmstudio', 'google/gemma-4-26b-a4b')

const { session } = await createAgentSession({
  cwd: '/Users/flavio/dev/tip-calculator',
  model,
  modelRuntime,
  tools: ['read', 'ls'],
  sessionManager: SessionManager.inMemory(),
})

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

await session.prompt('What does tip.js export? One sentence.')
console.log()
session.dispose()

ModelRuntime reads the same models.json and logins as the CLI. SessionManager.inMemory() means nothing gets saved to disk. The answer I got was “tip.js exports a function named tip that calculates the tip amount based on a bill and a percentage.”

A session has more methods than prompt(): steer() and followUp() do what Enter and Alt+Enter do in the terminal, setModel() switches models, and compact() shrinks the context. Extensions and skills load in SDK sessions too.

Keep it contained

Pi runs with your user’s permissions. It can read your SSH keys, delete your home folder and push to any repository you can push to. There’s no sandbox, and the docs are clear that project trust isn’t one either. Prompt injection from a README or a build log is a risk they don’t try to solve inside Pi.

For your own projects, with you watching, that’s the same deal you get from most agents in “full access” mode. For anything else (a repository you don’t know, a long run you won’t watch), the docs describe a few ways to isolate it:

  • Run the whole pi process in a Docker container, mounting only the project folder. The downside is that your API keys go inside the container too.
  • Use the Gondolin extension, which keeps Pi and your logins on the host and runs the tools inside a small Linux virtual machine, with the project mounted at /workspace.
  • Run it in a policy-controlled sandbox like OpenShell or Docker Sandboxes.

A remote machine is another option. I tried a few of those in my exe.dev deep dive. Fun fact: exe.dev donated the pi.dev domain.

Pi also contacts pi.dev at startup to check for a newer version, and it sends an anonymous ping after installs and updates. PI_OFFLINE=1 turns off both, along with every other startup network call.

How does Pi compare to other coding agents?

The closest agent to Pi is fx, Vercel Labs’ agent. Both are open source, both run in the terminal, both read AGENTS.md and skills, and both can be embedded in your own app. fx ships MCP, subagents and permission modes built in, while Pi leaves them to extensions.

Claude Code and Codex come from model companies, so they’re built around their own models and plans. They both have plan modes, subagents, MCP, hooks, plugins and permission controls out of the box. Codex also runs commands in a sandbox by default.

OpenCode is the other big open source terminal agent. It supports many providers like Pi does, but it comes with more: Build and Plan agents you switch with Tab, built-in subagents, MCP, per-tool permission rules, and an /undo command that reverts the file changes.

PifxClaude CodeCodex CLIOpenCode
LicenseMITApache-2.0Closed sourceApache-2.0MIT
Models15+ providers, several subscriptions, localVercel AI Gateway, Codex or Grok plan, local previewClaudeOpenAIMany providers, local
MCPExtensionBuilt inBuilt inBuilt inBuilt in
SubagentsExtension or packageBuilt inBuilt inBuilt inBuilt in
Permission promptsExtensionBuilt inBuilt inSandbox and approvalsBuilt in
Customize withTypeScript extensions, packagesSkills, MCP, libfxHooks, plugins, skills, MCPPlugins, hooks, skills, MCPPlugins, agents, MCP
Embed in your appSDK and RPClibfxAgent SDKSDKServer and SDK

Cursor’s agent is a different kind of tool. It lives in an editor first, the terminal version comes with it, and both run on your Cursor plan. I wrote about how these fit together in IDE, CLI or vibe coding tool.

My advice is to pick Pi if you like tuning your tools and want every model provider in one place, including your ChatGPT plan and local models. Pick Claude Code or Codex if you want the most polished experience with one vendor’s models and a sandbox you don’t have to set up. Pick OpenCode if you want an open source agent with the features already included.

Pi also works well next to the others. It’s a normal terminal program, so a terminal workspace like Herdr can run Pi, Claude Code and Codex in panes side by side. Herdr installs a small extension into ~/.pi/agent/extensions/ so it can show whether Pi is working or waiting for you.

How I use Pi

I started using Pi at the end of April 2026, still on the old @mariozechner package.

I logged in with my ChatGPT subscription and made GPT-5.5 the default, with the thinking level at minimal because most of what I asked was small. I added Claude Opus 4.7 to the scoped models, so Ctrl+P switched to it when a task needed more.

The first thing I did was ask Pi about itself. I wanted to know why the remotion-best-practices and wrangler skills showed up in the header, and where they were stored. Then I used it to reorganize my skills. It copied a few of them into ~/.agents/skills and replaced the old copies with symlinks, so each skill lives in one place.

Then the sled game. I started an empty folder and asked for “a 3D game about running dogs on sleds in the snow”. Pi set up Vite, TypeScript and Three.js, wrote the game with lane steering, obstacles, fish to collect and a score, and ran the build. I kept going from there, asking for the dogs to turn when they change lane and for an endless world to move around in. That’s also the session where I learned that /clone doesn’t bring your files back.

After that, Pi became my agent for small jobs:

  • In Backpack, an Astro and PocketBase app, I asked why login failed with “Something went wrong while processing your request”, and what the dozens of cli_migration_snapshot_* collections were. Then I had it delete them.
  • In a small CLI I wrote to find and kill local dev servers, I asked it to kill the leftover Netlify Edge Functions helper processes.
  • In another project, “create a GitHub repo here” was the whole prompt.
  • In my Notion skill, I asked it to switch from the Notion MCP server to Notion’s ntn CLI.

The Notion change is Pi’s philosophy in action: a CLI with good docs does the same job as an MCP server, without Pi needing MCP.

I also added the LM Studio provider you saw earlier, so I could switch to Gemma 4 from the same picker. It’s fine for questions about code on my machine. For changes, I stayed on GPT-5.5.

Pi isn’t the right tool for everything. I wouldn’t give it a repository I don’t know without a container, because nothing stops a command I didn’t expect. When I want a plan I can review before any file changes, Codex’s Plan mode is already there, while Pi needs a package. And for long unattended runs I’d rather have an agent with a real sandbox.

Where Pi fits me is small jobs in projects I know, plus the times I want the agent to behave differently. Then I write an extension, or ask Pi to write it, and /reload.

Tagged: AI · All topics

Want me to talk about your product? You can sponsor this site.

~~~

Related posts about ai: