Settings:
`hooks` and `customTools` arrays replaced with single `extensions` array
From the changelog
**Settings:** `hooks` and `customTools` arrays replaced with single `extensions` array
Between v0.30.0 and v0.35.0 (December 25, 2025 - January 5, 2026), pi underwent its most ambitious internal transformation. The 520 commits -- with v0.31.0 alone accounting for 313 -- restructured the session format into a tree, overhauled the agent runtime's package boundaries, unified hooks and custom tools into a single extensions system, and introduced the steer/followUp messaging semantics. This was pi looking at the extensibility surface it had built in Stage 5 and concluding that the abstractions were wrong. Not wrong in the sense of buggy, but wrong in the sense of creating unnecessary complexity that would compound over time.
The session tree was the most visible change. Sessions had always been linear: a sequence of messages appended to a JSONL file. Branching created a new file. This meant a conversation that explored three approaches to a problem produced three separate files with no connection between them. The tree format (v0.31.0) added id and parentId fields to every session entry, enabling in-place branching: navigate to any point with /tree, continue from there, and switch between branches while preserving all history in a single file. This required migrating every session on first load (v1 to v2), and later v2 to v3 when the extensions unification renamed hookMessage to custom.
The extensions unification (v0.35.0, #454) was equally radical. Hooks and custom tools had been separate concepts with separate discovery, separate APIs, separate documentation, and separate CLI flags. But they were both TypeScript modules exporting factory functions that received an API object. The new ExtensionAPI merged them: pi.on() for events, pi.registerTool() for tools, pi.registerCommand() for slash commands, pi.registerShortcut() for keybindings, pi.registerFlag() for CLI flags, and pi.registerMessageRenderer() for custom TUI rendering -- all in one module, one discovery path, one settings key.
The session tree design went through several iterations visible in the commit history: the initial design doc (351faef6), a switch from indices to UUIDs (64e7c80c), clarification of compaction in tree format (013b6814), session version fields with migrate-on-load strategy (3239a4a1), and finally the implementation (c58d5f20, 6f94e246). Each entry got an 8-character hex ID (95312e00), and SessionManager gained tree-aware methods: getTree(), getBranch(), getLeafId(), getChildren(), getLabel(). New entry types appeared: BranchSummaryEntry for context from abandoned branches, CustomEntry for extension state, CustomMessageEntry for extension-injected messages, and LabelEntry for bookmarks. The /tree command let users navigate between branches interactively, and the HTML export was rewritten to display the tree structure with a sidebar (173b81bc).
The pi-agent-core package underwent a fundamental restructuring in v0.31.0. The transport abstraction (ProviderTransport, AppTransport, AgentTransport) was removed entirely, replaced by a simple streamFn option for custom streaming implementations. messageTransformer became convertToLlm, and preprocessor became transformContext -- names that described what the functions did rather than where they sat in an abstract pipeline. The agent loop itself moved from pi-ai to pi-agent-core, consolidating the runtime in one package. AppMessage was renamed to AgentMessage everywhere, and the Attachment type was removed -- attachment handling became the responsibility of the convertToLlm function. The result was a package with five clean files: agent.ts, agent-loop.ts, types.ts, proxy.ts, and index.ts.
Version 0.32.0 (#403) split the queueMessage() API into two methods with distinct delivery semantics: steer() interrupted the agent mid-run (delivered after the current tool execution, skipping remaining tools), while followUp() waited until the agent finished naturally (delivered only when there were no more tool calls or steering messages). This replaced the ambiguous "queue mode" with explicit intent. The rename propagated through the entire stack: AgentLoopConfig callbacks, AgentSession methods, settings keys (queueMode to steeringMode), and hook APIs. A guard against concurrent prompt() calls (5ef3cc90) prevented the race conditions that had motivated the split.
The v0.35.0 release (#454) merged hooks and custom tools into extensions. The migration was systematic: HookAPI became ExtensionAPI, CustomToolFactory became ExtensionFactory, HookMessage became CustomMessage. Discovery was unified -- ~/.pi/agent/extensions/ replaced both hooks/ and tools/ directories. Slash commands were renamed to "prompt templates" (commands/ to prompts/) to avoid confusion with extension-registered commands via pi.registerCommand(). The package.json manifest support (9794868b) let complex extensions declare multiple entry points and npm dependencies, enabling extensions to be published as npm packages. Deprecation warnings guided users through the migration, and the commands/ directory was automatically renamed to prompts/ on startup.
The plan-mode hook (91fae8b2) demonstrated the power of the unified extension system. Activated via /plan, Shift+P shortcut, or --plan CLI flag -- all registered through pi.registerCommand(), pi.registerShortcut(), and pi.registerFlag() -- it restricted tools to read-only operations, showed a todo widget via ctx.ui.setWidget(), tracked progress via agent_end event parsing, and persisted state across sessions via pi.appendEntry(). The event bus (9c9e6822, #431) added a shared pub/sub channel (pi.events) for cross-extension communication, letting the plan-mode hook coordinate with other extensions without tight coupling.
Vertex AI with Application Default Credentials (214e7dae) was added, and the zAI provider was migrated from Anthropic to OpenAI-compatible API (#344). The enabledModels setting (#331) let users filter the model selector to show only models they cared about. Model generation became deterministic (#332) by sorting providers and models. These were not headline features, but they reflected a maturing provider layer where edge cases (expired OAuth tokens in long-running loops, #223) and ergonomic details (clickable OAuth login URLs, #349) received attention.
AGENTS.md at v0.35.0 was noticeably more prescriptive than earlier versions. New rules included "NEVER remove or downgrade code to fix type errors from outdated dependencies; upgrade the dependency instead" and "Always ask before removing functionality or code that appears to be intentional." These rules reflected lessons from the massive v0.31.0 refactoring -- when you rename hundreds of types and move code between packages, it is easy to accidentally delete something important. The changelog format gained explicit structure: Breaking Changes, Added, Changed, Fixed, Removed sections under [Unreleased], with a rule to read the existing unreleased section before adding entries to avoid duplicate subsections.
The extensions documentation (docs/extensions.md) opened with a remarkable line: "pi can create extensions. Ask it to build one for your use case." This was the project's vision distilled into a single sentence: pi was not just extensible by developers who read documentation, but by users who could describe what they wanted and let the agent write the extension. The ExtensionAPI was designed to be discoverable by LLMs -- clear method names, TypeBox schemas for parameters, consistent patterns across all registration methods.
The extensions unification and session tree created a stable foundation that later stages could build on without further structural upheaval. The package.json manifest for extensions pointed toward an npm-based distribution story where users could npm install community extensions. The steer/followUp distinction anticipated more sophisticated multi-agent coordination patterns where one agent needs to redirect another mid-task. And the plan-mode hook -- shipped as an example, not a built-in feature -- demonstrated that pi's most interesting capabilities could live outside its core, built by users and shared as extensions.
`hooks` and `customTools` arrays replaced with single `extensions` array
**Settings:** `hooks` and `customTools` arrays replaced with single `extensions` array
`--hook` and `--tool` flags replaced with `--extension` / `-e`
**CLI:** `--hook` and `--tool` flags replaced with `--extension` / `-e`
`hooks/`, `tools/` → `extensions/`; `commands/` → `prompts/`
**Directories:** `hooks/`, `tools/` → `extensions/`; `commands/` → `prompts/`
See type renames above
**Types:** See type renames above
See SDK migration above
**SDK:** See SDK migration above
Extensions can have their own `package.json` with dependencies (resolved via jiti)
Documentation: `docs/hooks.md` and `docs/custom-tools.md` merged into `docs/extensions.md`
Examples: `examples/hooks/` and `examples/custom-tools/` merged into `examples/extensions/`