Claude Code experience for the pi coding agent, in one package. Point pi at a project that already has a .claude/ directory and it reads your existing config: rules, commands, skills, hooks, output styles, MCP servers, and agents. It also adds the Claude Code features pi lacks: a todo overlay, checkpoints, memory, web search, and subagents.
What a repository ships is treated as untrusted until you approve it: project MCP servers, hooks, agents, rules, output styles, commands and skills load only once you say yes.
pi >=0.79.1 (0.84.x recommended) and Node >=22.19 for current pi.
pi install npm:pi-code # from npm
pi install -l npm:pi-code # project-local instead, writes .pi/settings.jsonOther sources:
pi install git:github.com/ilovepixelart/pi-code
pi install ./pi-code # local checkout, then /reload after editsOne pi install and everything below loads on the next start. pi list shows what is installed, pi config toggles individual resources, and pi update pi-code upgrades it. Each feature is an extension under extensions/.
| Feature | Reads / provides | Extension |
|---|---|---|
| Global + project rules | ~/.claude/rules, .claude/rules (nearest at or above cwd); unscoped rules inlined in full, paths:-scoped rules surfaced as pointers and auto-attached (the rule body is appended to a read/edit/write result when a matching file is touched, once per rule per session) |
claude-rules.ts |
| Custom slash commands | .claude/commands/**/*.md (namespaced /dir:name); $ARGUMENTS (with the ARGUMENTS: append when unused), 0-based $ARGUMENTS[N]/$N, named arguments: frontmatter, ${CLAUDE_SESSION_ID}/${CLAUDE_EFFORT}/${CLAUDE_SKILL_DIR}/${CLAUDE_PROJECT_DIR} (in bodies and allowed-tools rules); !`cmd` and multi-line ```! bash (whitespace-bounded, merged stderr, 2-minute budget, a failure aborts the invocation with the documented exit-1 carveout), @file inlining; allowed-tools with Bash(...) and Read/Edit/Write path scopes enforced at call time (gitignore anchors, Edit governs writes), disallowed-tools, argument-hint, model (switches the session model for the command's turn, restored after), effort (raises reasoning for the turn, restored after), shell: powershell (injected spans run through PowerShell when a pwsh binary is present, else /bin/sh); the model can also run a command itself through the SlashCommand tool (Claude's SlashCommand in allowed-tools), steered by when_to_use and opted out per file with disable-model-invocation (user-invocable: false hides a command from the menu while still exposing it to the model); disableSkillShellExecution (managed and user always, project when trusted) replaces every ! span with a policy-disabled placeholder; project commands gated on approval |
commands.ts |
/init |
generates a project context file: detects an existing AGENTS.md/CLAUDE.md (proposes improvements) or none (creates AGENTS.md, pi's preferred name), ingesting .cursor/rules, .cursorrules, and .github/copilot-instructions.md when present; drives the main agent with full tools via a prompt (not a tool-less completion) so it analyzes the codebase and writes the file itself |
init.ts |
| Skills | .claude/skills → pi skill discovery, project skills gated on approval (pi reads name, description, disable-model-invocation; allowed-tools is inert in pi's loader) |
skills.ts |
| Hooks | .claude/settings.json hooks: PreToolUse (blocks, rewrites input via updatedInput), PostToolUse (feedback and additionalContext land next to the tool result), PostToolUseFailure, SessionStart (context injection), UserPromptSubmit (blocks and injects context), Stop (a block continues the conversation), SubagentStart/SubagentStop, PreCompact, PostCompact, SessionEnd, Notification (idle_prompt, the type pi can source), InstructionsLoaded (observational: fires per loaded context file at session start, plus path_glob_match on a scoped-rule attach and include per resolved @import; deduped per session; nested_traversal/compact reasons never fire since pi does not lazily load nested CLAUDE.md or reload after compaction); type: http entries POST the payload (a 2xx JSON body renders the decision, everything else is non-blocking per Claude's contract), type: prompt evaluates in-process against the session model, type: mcp_tool calls a connected server's tool, and type: agent (experimental) spawns a read-only Read/Grep/Glob subagent that returns the JSON decision (a missing model/server/runner is non-blocking, only a PreToolUse timeout fails closed); Claude matcher semantics incl. mcp__server__tool names; payloads carry session_id, transcript_path, cwd, permission_mode, effort; permissionDecision: "ask" prompts via a confirm dialog (blocks when headless), SubagentStop/PostToolUseFailure are notify-only, and a timed-out PreToolUse/UserPromptSubmit hook fails closed at a 60s default (Claude: 600s, non-blocking) since pi has no permission backstop; a command hook may use exec form (command as an argv array, run with no shell) and executes with CLAUDECODE=1 and CLAUDE_PROJECT_DIR set; a user-typed !/!! bash line runs PreToolUse (there is no PostToolUse for it); type: http targets are gated by allowedHttpHookUrls (union of managed and settings scopes; unset allows all, [] blocks every http hook); a Stop hook may block at most 8 times before the turn ends (CLAUDE_CODE_STOP_HOOK_BLOCK_CAP); disableAllHooks in any scope turns the system off; /hooks prints the resolved configuration |
hooks.ts |
| Output styles | .claude/output-styles + active outputStyle; plus styles shipped by enabled plugins (manifest outputStyles, default output-styles/, ranked below the user's and project's own); Claude replace semantics with keep-coding-instructions; bundled Explanatory/Learning/Proactive; /output-style [name] |
output-styles.ts |
CLAUDE.md @imports and rewriting |
resolves @path imports pi's native loader skips (4-hop depth, budget-capped); loads the user ~/.claude/CLAUDE.md and the project .claude/CLAUDE.md (approval-gated, deduped against the repo-root CLAUDE.md/AGENTS.md pi loads natively) that pi's own loader does not; loads every CLAUDE.local.md from the repo root down to cwd (approval-gated, root first); injects the managed CLAUDE.md (a per-OS file beside managed-settings.json) and the claudeMd string from managed-settings.json at the top of context (managed content is never excludable); honors claudeMdExcludes (glob/absolute-path skip list from user, approved-project, and managed settings, merged; managed content never excluded); strips block-level HTML comments from CLAUDE.md, rule, and imported bodies (fenced-code comments preserved), so a commented-out @import does not expand; with CLAUDE_CODE_ADDITIONAL_DIRECTORIES_CLAUDE_MD set, loads CLAUDE.md/.claude/CLAUDE.md/.claude/rules/*.md/CLAUDE.local.md from each --add-dir directory (comma-separated for several, since pi's flag is single-value) |
context-imports.ts |
Settings env |
env blocks from managed-settings.json, ~/.claude/settings.json, and the project .claude/settings.json/settings.local.json exported into the session (per-key managed > user > project); the project scope is approval-gated (a repo's env can redirect providers), a shell export outranks user and project but a managed key overrides even that, and a key an approved project set is unset once a later session no longer defines it |
env-settings.ts |
| MCP servers | user ~/.claude.json (incl. per-project projects[cwd] local scope), ~/.pi/agent/mcp.json; project .mcp.json, .pi/mcp.json (once approved; enabledMcpjsonServers/disabledMcpjsonServers/enableAllProjectMcpServers honored, consent keys only from non-repo settings); stdio/HTTP/SSE/WebSocket by type (WebSocket is url-only: any headers/bearerToken/headersHelper on a ws server is ignored with a warning); ${VAR:-default} expansion; a headersHelper command whose stdout JSON merges into the transport headers (http/sse); a managed-mcp.json beside managed-settings.json takes exclusive control (only its servers load, every other scope and the approval flow suppressed; an empty file disables MCP); managed allowedMcpServers/deniedMcpServers read from managed-settings.json only ({serverName} entries, applied globally across scopes, allow list exclusive with an empty array = lockdown, deny wins); connect and per-call budgets (MCP_TIMEOUT/MCP_TOOL_TIMEOUT) over a 4-hour wall default, plus a 5-minute idle timeout that a progress notification resets (CLAUDE_CODE_MCP_TOOL_IDLE_TIMEOUT, 0 disables); a connected server's prompts register as /mcp__server__prompt commands and its resources are reachable through the list_mcp_resources/read_mcp_resource tools; tools refresh on list_changed; bearer tokens, or OAuth for remote servers (browser login on 401 after a confirm, CSRF-guarded loopback callback, tokens under ~/.pi/agent/mcp-oauth, silent refresh later) |
mcp.ts |
| Project trust | prompts before loading project config (MCP servers, hooks, agents, rules, output styles, commands, skills) that pi would otherwise trust silently | internal/project-approval.ts |
| Subagents / Task | builtin Explore/Plan/general-purpose agents, ~/.claude/agents and ~/.pi/agent/agents, plus project .claude/agents and .pi/agents (scanned recursively into subfolders) merged by default once the project is trusted (project wins on a name clash); frontmatter tools/disallowedTools/model (sonnet/opus/haiku/fable tier aliases or a concrete id)/effort/skills preload/permissionMode: plan/maxTurns/memory (user/project/local give the child its own persistent store under .claude/agent-memory[-local], injected with Read/Write/Edit enabled, gated on auto memory; the parent conversation's memory is never loaded into a subagent, matching Claude); parallel and chain modes (pi extensions), one nesting level; background runs with cancel and resume (/tasks lists them, /agents lists the discovered roster) |
subagent/ |
| Plan mode | plan_mode_complete tool, tool snapshot/restore that survives /reload |
plan-mode/ |
| Todo list | persistent overlay, status machine, compaction-safe | todo.ts |
| Checkpoints / rewind | shadow-repo snapshots; restore overwrites checkpointed files, keeps files created later; 100 per session, repos pruned after 30 days | git-checkpoint.ts |
| Persistent memory | per-repo memories under ~/.pi/agent/memory keyed on the repository root (subdirectory sessions share one store, as Claude does; pi's own store, separate from Claude's), index injected each session within Claude's 200-line/25KB bound (YAML frontmatter and block HTML comments stripped before it counts or loads); a save that would overflow it reports why; a memory written with frontmatter gets a modified: ISO timestamp; honors autoMemoryEnabled (settings) and CLAUDE_CODE_DISABLE_AUTO_MEMORY (env) to turn it off, and autoMemoryDirectory (absolute or ~/) to relocate the store |
memory.ts |
| WebSearch / WebFetch | key-free DuckDuckGo search (with allowed_domains/blocked_domains); SSRF-guarded fetch that prefers markdown via Accept then converts HTML, with Claude's 15-minute per-URL cache and an optional prompt that runs the page through the model in-process and returns the answer (falls back to markdown when headless or on error) |
web.ts |
| AskUserQuestion | 1-4 questions per call (asked in sequence), each with header and 2-4 options, single- or multiSelect, plus free-text |
question.ts |
| Statusline | Claude statusLine command contract (stdin JSON incl. version, hook_event_name, session_name, a cost block with wall and API durations and lines added/removed, a context_window token breakdown, exceeds_200k_tokens, and rate_limits carrying the five_hour/seven_day utilization and reset from the provider's rate-limit headers; padding, refreshInterval, 300ms debounce); built-in turn state + session cost fallback |
status-line.ts |
| Notifications | terminal notification when a turn ends (OSC 777 / Kitty OSC 99 / Windows toast); honors preferredNotifChannel (terminal_bell, notifications_disabled, iterm2_with_bell, else desktop) from user settings; fires only when you "appear to be away" (approximated by turn duration, since pi exposes no terminal-focus signal) |
notify.ts |
| Think keywords | raises reasoning for one turn when the prompt carries a keyword: ultrathink to the max, think hard/think harder to high, a bare think to medium; only ever raises, matched on word boundaries, and the prior level is restored once the turn settles |
thinking.ts |
| Session title | names a new session from its first message with a single model call (shown in the session selector and the terminal title); never overwrites an existing name, best-effort, at most once per session | session-title.ts |
/context |
reports how much of the model's context window the session occupies (used, window, free, and percent), and reads pi's live usage so it reflects a compaction | context-usage.ts |
| Claude plugins | installed marketplace plugins (~/.claude/plugins/cache), active per enabledPlugins in user settings only (a checked-out repo cannot flip which code-bearing plugins run, so project settings never toggle them): commands as /plugin:name, agents, hooks and MCP servers (tools aliased mcp__plugin_<plugin>_<server>__<tool>) and output styles with ${CLAUDE_PLUGIN_ROOT}/${CLAUDE_PLUGIN_DATA} and ${user_config.KEY} (from pluginConfigs[id].options in user settings) substituted; skill dirs contribute too, though pi's loader names them without the plugin prefix |
internal/plugins.ts |
Slash commands: /init, /context, /memory, /todos, /rewind, /tasks, /agents, /plan, /mcp, /hooks, and /output-style, alongside your own /dir:name commands, /skill:name skills, /plugin:name plugin commands, and each connected server's /mcp__server__prompt prompts.
pi has no general permission system, so most of what Claude routes through a permission prompt maps to hard behavior here: allowed-tools restricts the turn's tool set instead of pre-approving calls, and a hook that times out on PreToolUse or UserPromptSubmit fails closed. A hook's permissionDecision: "ask" is the exception: it shows a confirm dialog and lets the call through when you approve (a headless run has no dialog, so it blocks). Where a Claude restriction cannot be expressed at all (an argument-scoped grant in an agent's tools:), the definition is rejected rather than widened.
CLAUDE.md itself needs no extension: pi loads CLAUDE.md / AGENTS.md context files natively (global + walking cwd to root). context-imports.ts only adds the @import resolution pi's loader lacks, appending the imported files without re-injecting the base. Setting CLAUDE_CONFIG_DIR relocates the entire home config scope (settings, commands, agents, skills, plugins, output styles, memory, and the user CLAUDE.md); a project's own .claude/ is a separate scope and is unaffected.
extensions/internal/ holds shared modules pi's loader must not treat as extensions: output-guard.ts (context-budget truncation), web-transport.ts (DNS-pinned fetch), project-approval.ts (the trust decision above), command-file.ts (slash-command parsing and dynamic content), config-dir.ts (the CLAUDE_CONFIG_DIR home-scope resolver), managed-settings.ts (the enterprise policy file), model-complete.ts (the in-process model calls behind session titling and prompt hooks), mcp-oauth.ts (the OAuth loopback), and the shared-bus contracts mcp-alias.ts, plan-mode-state.ts and subagent-events.ts. The extensions use them; only internal/ keeps them out of pi's extension scan.
Vendored bases (question, notify, status-line) come from pi's MIT example extensions (see LICENSE).
npm install
npm run check # biome + strict tsc + vitest, the whole gate
scripts/e2e.sh # quick smoke of the real pi TUI via tmux (needs a working model)
scripts/e2e-full.sh # every README feature end to end, model turns included (5-15 min)
scripts/record-demos.sh # re-records demos/*.tape with vhs at low thinkingExtensions live in extensions/, tests in tests/. Install a local checkout with pi install ./pi-code, then /reload after edits. Development needs Node >=22.19.
Issues and pull requests are welcome. See CONTRIBUTING.md for setup, the test and review conventions, and the release process, and the Code of Conduct for community expectations. Report security issues privately as described in SECURITY.md. Release notes are on the Releases page.
MIT.