Roman Imankulov

Roman Imankulov

Full-stack Python developer. Building Smello.

search results (esc to close)
07 Jun 2026

The Pi version

The Pi version

I don’t know Pi well. I used it for a few small tasks and appreciated how minimal it felt. My news reader has become my test task for trying different agent tools against the same problem, so I rebuilt it with Pi to explore further. My opinions here may not survive deeper use, but the way I think about it: Claude Code is Django, Pi is Flask.

Claude Code ships with WebFetch, WebSearch, sub-agents, MCP support, a permissions system, and a whole settings infrastructure. You get a lot for free. The tradeoff is that you can’t easily remove what you don’t need. You may not use MCP or sub-agents, but they’re still there in the system prompt and the runtime. Like Django’s ORM, you lose more from fighting the built-in pieces than from just using them.

Pi gives you seven built-in tools (read, write, edit, bash, grep, find, ls) and an extension system. Everything else you build yourself as TypeScript functions, and the agent only sees what you explicitly hand it.

The same workflow, assembled differently

In the Claude Code version, the news reader has three moving parts: an MCP server (Python) that validates and stores items, a sub-agent definition (markdown) that restricts what the fetcher can do, and a skill file (markdown) that orchestrates the whole thing. These live in different directories and connect through naming conventions and config files.

Pi needs two things: an extension that registers custom tools, and a system prompt that tells the agent what to do. Here’s the project:

.pi/
├── extensions/
   └── news-tools.ts     # custom tools: web_fetch, save_news_item
└── SYSTEM.md              # replaces the default system prompt

.pi/SYSTEM.md replaces Pi’s default system prompt entirely. The default is a short paragraph about being a coding assistant, plus guidelines for file exploration. For a single-purpose automation, I swap it for task-specific instructions:

You are a news scraper agent. Your job is to fetch tech news
and save relevant items.

Fetch the front page of Hacker News (news.ycombinator.com) and
Lobsters (lobste.rs). Identify items relevant to: Python,
developer tools, AI/ML, and software architecture.

For each relevant item, call save_news_item with title, url,
source, tags, summary, and discussion_url.

Skip items about business/funding, social media drama, or
topics unrelated to the interests listed above.

Then run it:

pi -p --no-builtin-tools --no-context-files --no-session "Go"

-p is print mode (non-interactive, exits when done). --no-builtin-tools disables read/write/edit/bash so the agent can only use the extension tools. The other flags skip context files and session persistence for a clean one-shot run. The user message is just a trigger.

Building your own WebFetch

Pi has no way to fetch a web page, so you build it yourself. Here’s the web_fetch tool from the extension:

import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
import { Type } from "typebox";
import TurndownService from "turndown";

const ALLOWED_DOMAINS = ["news.ycombinator.com", "lobste.rs"];
const turndown = new TurndownService();

export default function (pi: ExtensionAPI) {
  pi.registerTool({
    name: "web_fetch",
    label: "Fetch URL",
    description: `Fetch a web page and return content as markdown. Allowed domains: ${ALLOWED_DOMAINS.join(", ")}`,
    parameters: Type.Object({
      url: Type.String({ description: "URL to fetch" }),
    }),
    async execute(_id, params) {
      const hostname = new URL(params.url).hostname;
      if (!ALLOWED_DOMAINS.includes(hostname)) {
        throw new Error(`Domain not allowed: ${hostname}`);
      }
      const resp = await fetch(params.url);
      const html = await resp.text();
      return {
        content: [{ type: "text", text: turndown.turndown(html) }],
        details: {},
      };
    },
  });

  // save_news_item: writes a JSON file to data/
  // (full source on GitHub)
}

The same extension also registers save_news_item, which writes structured JSON to data/. The full source is about 20 lines.

The only project dependency is turndown. Pi is installed globally, but Node module resolution is path-based: when your extension imports turndown, Node finds it in your project’s node_modules. Pi’s own packages (typebox, the extension API) resolve from Pi’s global install.

Domain restriction in Claude Code is WebFetch(domain:news.ycombinator.com), a magic string the runtime parses and enforces. In Pi, it’s an if-check you wrote.

Claude Code’s WebFetch also runs an LLM pass on fetched HTML to extract cleaner content from complex layouts. My tool uses turndown, which is purely mechanical. For Hacker News and Lobsters front pages, this is plenty.

What you gain by having less

In the Claude Code version, save_news_item is a FastMCP server: a separate Python file with a Pydantic model, registered via .mcp.json, spawned as a subprocess. MCP gave me schema validation and tool discovery, but also a protocol layer, JSON-RPC serialization, and a config file.

Pi’s registerTool() is just a function with a TypeBox schema (Pydantic for TypeScript). TypeBox compiles to standard JSON Schema, so the LLM sees the same tool definition it would from Pydantic or MCP, just without the subprocess. Pi chose not to include MCP because it adds a protocol layer without clear benefit for single-user CLI agents.

The same minimalism helps with security. The Claude Code version uses sub-agents for isolation: the news-fetcher sub-agent can only access WebFetch and save_news_item, so even if a prompt injection fires from a Hacker News title, it can’t read files or run shell commands. This requires a permissions layer and tool allowlists.

In Pi, I run the agent with --no-builtin-tools, leaving only the extension tools. You get the same isolation with a CLI flag instead of a sub-agent definition file.

Pi also supports 30+ model providers, so you’re not locked into a single vendor the way you are with Claude Code. You can experiment with cheaper models for tasks that don’t need the best reasoning.

On top of that, Pi can use existing subscriptions as providers. I use ChatGPT Plus/Pro (Codex Subscription) as a provider, so I can run this scraper with GPT models at no extra per-token cost. Instead of paying per-token through Claude Code, it’s part of the subscription I’m already paying for.

What I like and what I miss

I like Pi’s transparency. When something goes wrong, there are fewer layers to debug through.

For a narrow automation like this, Pi’s minimalism is the right fit. But for a full coding session where you want web search, sub-agents, and a permission system keeping you safe while you experiment, batteries are the way to go. Each missing piece in Pi might be 10-20 lines of code, but they add up. Sub-agents, for example, are possible (Pi’s SDK has createAgentSession()), but building them for a task this simple would look overengineered.

The full project is on GitHub.

Roman Imankulov

Hey, I am Roman

I am a full-stack Python developer. Currently building Smello, a local-first HTTP traffic inspector for Python.

More about me