| name | pi-coding-agent-extensions |
| description | Build and use custom extensions for Pi Coding Agent, the open-source alternative to Claude Code |
| triggers | ["how do I extend pi coding agent","create a custom pi extension","add a widget to pi agent","build a pi coding agent extension","customize pi agent ui","make a pi agent tool","orchestrate multiple pi agents","pi agent communication between agents"] |
Pi Coding Agent Extensions
Skill by ara.so — Claude Code Skills collection.
Overview
Pi Coding Agent is an open-source terminal-based AI coding assistant similar to Claude Code. This project (pi-vs-claude-code) provides a collection of production-ready extensions that showcase Pi's customization capabilities: custom UI widgets, multi-agent orchestration, safety auditing, cross-agent integrations, and inter-agent communication.
Extensions are TypeScript files that hook into Pi's lifecycle events to add tools, modify UI, intercept commands, and coordinate with other agents.
Prerequisites
All three are required:
curl -fsSL https://bun.sh/install | bash
brew install just
Installation
git clone https://github.com/disler/pi-vs-claude-code.git
cd pi-vs-claude-code
bun install
cp .env.sample .env
API Key Setup
Pi does NOT auto-load .env files. You must source keys before launching:
Option 1 - Manual source:
source .env && pi
Option 2 - Shell alias (add to ~/.zshrc):
alias pi='source $(pwd)/.env && pi'
Option 3 - Use just (automatic):
just pi
Running Extensions
Single Extension
pi -e extensions/minimal.ts
Stack Multiple Extensions
pi -e extensions/minimal.ts -e extensions/cross-agent.ts
Using Just Recipes
just
just ext-minimal
just ext-tool-counter
just ext-subagent-widget
just ext-agent-team
just ext-damage-control
just ext-pi-pi
just coms-net-server
just coms
just all
Extension Architecture
Extensions export a PiExtension object with lifecycle hooks:
import type { PiExtension, PiApp } from "pi";
export default {
name: "my-extension",
version: "1.0.0",
async init(app: PiApp) {
},
async beforeTurn(app: PiApp, turn: Turn) {
},
async afterTurn(app: PiApp, turn: Turn) {
},
async destroy(app: PiApp) {
}
} satisfies PiExtension;
Key Extension Examples
Minimal Footer
Compact UI showing model name and context usage:
import type { PiExtension, PiApp } from "pi";
import { Widget } from "pi/ui";
export default {
name: "minimal",
version: "1.0.0",
async init(app: PiApp) {
const footer = new Widget({
position: "footer",
render: () => {
const model = app.session.model.name;
const usage = app.session.contextUsage;
const pct = Math.round(usage * 100);
const filled = Math.round(usage * 10);
const bar = "█".repeat(filled) + "░".repeat(10 - filled);
return `${model} [${bar}] ${pct}%`;
}
});
app..(footer);
}
} ;
Custom Tool Registration
Add a new tool the agent can call:
import type { PiExtension, PiApp } from "pi";
export default {
name: "custom-tool",
version: "1.0.0",
async init(app: PiApp) {
app.registerTool({
name: "analyze_code_quality",
description: "Analyze code quality metrics for a file",
parameters: {
type: "object",
properties: {
file_path: {
type: "string",
description: "Path to the file to analyze"
}
},
required: ["file_path"]
},
handler: async (args: { file_path: string }) => {
const content = await app.fs.readFile(args.file_path);
return {
lines: content.split("\n").length,
complexity: calculateComplexity(content),
: (content)
};
}
});
}
} ;
(): {
branches = (code.() || []).;
branches + ;
}
(): [] {
: [] = [];
(code.()) issues.();
(code.()) issues.();
issues;
}
Live Widget Above Editor
Create a persistent widget that updates in real-time:
import type { PiExtension, PiApp, Turn } from "pi";
import { Widget } from "pi/ui";
const toolCounts = new Map<string, number>();
export default {
name: "tool-counter-widget",
version: "1.0.0",
async init(app: PiApp) {
const widget = new Widget({
position: "above-editor",
height: 3,
render: () => {
const entries = Array.from(toolCounts.entries())
.sort((a, b) => b[1] - a[1])
.slice(0, 5);
return entries
.map(([tool, count]) => `${tool}: `)
.() || ;
}
});
app..(widget);
},
() {
( call turn. || []) {
toolCounts.(call., (toolCounts.(call.) || ) + );
}
app..();
}
} ;
Safety Auditing (Damage Control)
Intercept and block dangerous commands before execution:
import type { PiExtension, PiApp, Turn, ToolCall } from "pi";
import { readFileSync } from "fs";
import { parse } from "yaml";
interface DamageControlRules {
blocked_patterns: string[];
allowed_paths: string[];
}
export default {
name: "damage-control",
version: "1.0.0",
async beforeTurn(app: PiApp, turn: Turn) {
const rules: DamageControlRules = parse(
readFileSync(".pi/damage-control-rules.yaml", "utf-8")
);
for (const call of turn.toolCalls || []) {
if (call.name === "bash") {
const cmd = call.arguments.command;
for ( pattern rules.) {
(cmd.(pattern)) {
(
);
}
}
(cmd.() && call..) {
isAllowed = rules..(
call...(allowed)
);
(!isAllowed) {
(
);
}
}
}
}
}
} ;
Example .pi/damage-control-rules.yaml:
blocked_patterns:
- "rm -rf /"
- "dd if="
- ":(){ :|:& };:"
- "curl | bash"
- "wget | sh"
allowed_paths:
- "/tmp"
- "./src"
- "./tests"
- "./extensions"
Multi-Agent Orchestration
Subagent Spawning (/sub)
Offload tasks to background Pi agents:
import type { PiExtension, PiApp } from "pi";
import { Widget } from "pi/ui";
import { spawn } from "child_process";
const subagents = new Map<string, any>();
export default {
name: "subagent-widget",
version: "1.0.0",
async init(app: PiApp) {
app.registerCommand({
name: "sub",
description: "Spawn a background Pi agent for a task",
handler: async (args: string[]) => {
const task = args.join(" ");
const id = `sub-${Date.now()}`;
const widget = new Widget({
position: "above-editor",
height: 4,
:
});
app..(widget);
proc = (, [, task], {
: process.,
: [, , ]
});
subagents.(id, { proc, : , widget });
proc..(, {
subagents.(id). = data.().(-);
app..();
});
proc.(, {
subagents.(id). = ;
app..();
( {
app..(widget);
subagents.(id);
}, );
});
}
});
}
} ;
Usage:
/sub implement user authentication with bcrypt
Agent Team Orchestration
Dispatcher pattern with specialist agents:
import type { PiExtension, PiApp } from "pi";
import { readFileSync } from "fs";
import { parse } from "yaml";
import { execSync } from "child_process";
interface TeamConfig {
agents: {
[key: string]: {
description: string;
system_prompt: string;
model?: string;
}
};
}
export default {
name: "agent-team",
version: "1.0.0",
async init(app: PiApp) {
const config: TeamConfig = parse(
readFileSync(".pi/agents/teams.yaml", "utf-8")
);
app.registerTool({
name: "dispatch_agent",
description: "Delegate a task to a specialist agent",
parameters: {
type: "object",
: {
: {
: ,
: .(config.),
:
},
: {
: ,
:
}
},
: [, ]
},
: (: { : ; : }) => {
agent = config.[args.];
(!agent) ();
agentPath = ;
(agentPath, .({
: agent.,
: agent. || app...
}));
result = (
,
{ : , : process. }
);
{
: args.,
: result
};
}
});
}
} ;
Example .pi/agents/teams.yaml:
agents:
backend:
description: Node.js/TypeScript backend specialist
system_prompt: |
You are an expert backend engineer specializing in Node.js, TypeScript,
Express, and PostgreSQL. Focus on API design, database schemas, and server architecture.
model: gpt-4o
frontend:
description: React/Next.js frontend specialist
system_prompt: |
You are an expert frontend engineer specializing in React, Next.js, and Tailwind.
Focus on component architecture, state management, and responsive design.
model: claude-opus-4
security:
description: Security auditing specialist
system_prompt: |
You are a security expert. Audit code for vulnerabilities, suggest fixes,
and ensure best practices for authentication, authorization, and data protection.
model: gpt-4o
Pi-to-Pi Communication
Local Communication (Same Machine)
Unix socket-based peer-to-peer messaging:
import type { PiExtension, PiApp } from "pi";
import { createServer, connect } from "net";
import { existsSync, mkdirSync, unlinkSync } from "fs";
const SOCKET_DIR = "/tmp/pi-coms";
const messages = new Map<string, string[]>();
export default {
name: "coms",
version: "1.0.0",
async init(app: PiApp) {
if (!existsSync(SOCKET_DIR)) mkdirSync(SOCKET_DIR);
const socketPath = `${SOCKET_DIR}/${app.session.id}.sock`;
const server = createServer((socket) => {
socket.on("data", (data) => {
const msg = JSON.parse(data.());
(!messages.(msg.)) messages.(msg., []);
messages.(msg.)!.(msg.);
});
});
server.(socketPath);
app.({
: ,
: ,
: { : , : {} },
: () => {
agents = ()
.( f.())
.( f.(, ));
{ agents };
}
});
app.({
: ,
: ,
: {
: ,
: {
: { : , : },
: { : , : }
},
: [, ]
},
: (: { : ; : }) => {
targetSocket = ;
client = (targetSocket);
client.(.({
: app..,
: args.
}));
client.();
{ : };
}
});
app.({
: ,
: ,
: { : , : {} },
: () => {
all = .(messages.()).( ({
,
: msgs
}));
messages.();
{ : all };
}
});
},
() {
socketPath = ;
((socketPath)) (socketPath);
}
} ;
Network Communication (Cross-Machine)
HTTP/SSE hub for LAN or remote agent coordination:
Start the hub server:
just coms-net-server
export PI_COMS_NET_AUTH_TOKEN=your-secret-token
just coms-net-server-lan
Connect agents:
just coms1
just coms2
The extension (extensions/coms-net.ts) provides tools:
coms_net_list - List connected agents
coms_net_send - Send message to specific agent
coms_net_broadcast - Broadcast to all agents
coms_net_get - Retrieve pending messages
Cross-Agent Integration
Load commands and skills from Claude Code, Gemini, Codex directories:
import type { PiExtension, PiApp } from "pi";
import { readdirSync, readFileSync, existsSync } from "fs";
export default {
name: "cross-agent",
version: "1.0.0",
async init(app: PiApp) {
const agentDirs = [".claude", ".gemini", ".codex"];
for (const dir of agentDirs) {
if (!existsSync(dir)) continue;
const skillsDir = `${dir}/skills`;
if (existsSync(skillsDir)) {
for (const file of readdirSync(skillsDir)) {
if (!file.endsWith(".md")) continue;
const content = readFileSync(`${skillsDir}/${file}`, "utf-8");
app.addContextDocument({
name: ,
content
});
}
}
agentsDir = ;
((agentsDir)) {
( file (agentsDir)) {
(!file.()) ;
agentName = file.(, );
systemPrompt = (, );
app.({
: ,
: ,
: () => {
app.. = systemPrompt;
;
}
});
}
}
}
}
} ;
Creating Your Own Extension
1. Create Extension File
touch extensions/my-feature.ts
2. Implement Extension
import type { PiExtension, PiApp } from "pi";
export default {
name: "my-feature",
version: "1.0.0",
description: "What this extension does",
async init(app: PiApp) {
console.log("Extension initialized");
},
async beforeTurn(app: PiApp, turn: Turn) {
},
async afterTurn(app: PiApp, turn: Turn) {
},
async destroy(app: PiApp) {
}
} satisfies PiExtension;
3. Add Just Recipe
Edit justfile:
ext-my-feature:
set -euo pipefail
source .env
pi -e extensions/my-feature.ts
4. Test Extension
just ext-my-feature
Configuration Files
.pi/settings.json
Pi workspace settings:
{
"model": "gpt-4o",
"temperature": 0.7,
"maxTokens": 4096,
"systemPrompt": "You are a helpful coding assistant.",
"theme": "default"
}
.pi/damage-control-rules.yaml
Safety rules for damage-control extension:
blocked_patterns:
- "rm -rf /"
- "> /dev/sda"
- "dd if="
- "mkfs"
- "curl | bash"
allowed_paths:
- "./src"
- "./tests"
- "./extensions"
- "/tmp"
max_file_size_mb: 10
.pi/agents/teams.yaml
Agent team definitions:
agents:
researcher:
description: Research and documentation specialist
model: gpt-4o
system_prompt: |
You are a research specialist. Your job is to find accurate, up-to-date
information from documentation, APIs, and code examples.
implementer:
description: Code implementation specialist
model: claude-opus-4
system_prompt: |
You are an implementation specialist. Given requirements and research,
you write clean, tested, production-ready code.
Common Patterns
Persistent State Across Turns
const state = {
taskList: [] as string[],
completedCount: 0
};
export default {
async init(app: PiApp) {
app.registerTool({
name: "add_task",
parameters: {
type: "object",
properties: {
task: { type: "string" }
},
required: ["task"]
},
handler: async (args: { task: string }) => {
state.taskList.push(args.task);
return { total: state.taskList.length };
}
});
},
async afterTurn(app: PiApp, turn: Turn) {
console.log(`Tasks: ${state.taskList.length}`);
}
} satisfies PiExtension;
Dynamic Widget Updates
const widget = new Widget({
position: "footer",
render: () => dynamicContent
});
let dynamicContent = "Initial";
setInterval(() => {
dynamicContent = new Date().toISOString();
app.ui.refresh();
}, 1000);
Command Registration
app.registerCommand({
name: "mycommand",
description: "Does something useful",
handler: async (args: string[]) => {
const result = await doSomething(args);
return result;
}
});
Usage in Pi:
/mycommand arg1 arg2 arg3
Troubleshooting
Extension Not Loading
Check TypeScript syntax:
bun run tsc --noEmit extensions/my-extension.ts
Verify extension export:
export default { ... } satisfies PiExtension;
API Keys Not Working
Verify environment:
echo $OPENAI_API_KEY
Check .env file:
cat .env
source .env
Widget Not Appearing
Check position:
const widget = new Widget({ position: "footer", ... });
Call refresh after updates:
widget.content = "New content";
app.ui.refresh();
Tool Not Being Called
Verify description clarity:
app.registerTool({
name: "my_tool",
description: "Clear, specific description of WHEN to use this tool",
});
Check parameter schema:
parameters: {
type: "object",
properties: {
required_param: {
type: "string",
description: "What this parameter does"
}
},
required: ["required_param"]
}
Subagent Communication Failing
Check socket directory:
ls -la /tmp/pi-coms/
Verify session IDs:
console.log("My session ID:", app.session.id);
For coms-net, check server is running:
curl http://localhost:3141/health
Advanced: Building Multi-Step Workflows
Combine extensions for complex workflows:
pi -e extensions/damage-control.ts \
-e extensions/agent-team.ts \
-e extensions/tool-counter-widget.ts
Create a custom workflow recipe in justfile:
workflow-full-stack:
set -euo pipefail
source .env
pi \
-e extensions/agent-team.ts \
-e extensions/damage-control.ts \
-e extensions/tool-counter.ts \
-e extensions/cross-agent.ts \
-p "Build a full-stack app with backend and frontend teams"
Resources
Contributing
Extensions in this repository are examples. To contribute:
- Create a new extension in
extensions/
- Add a just recipe in
justfile
- Document in a spec file under
specs/
- Submit a PR with working examples