# pi - Extension Development pi extensions share one registration space with weak conflict diagnostics. Footers and tool names collide silently, the extension factory re-runs on session replacement, and configuration mistakes fail without an error message, so validate every extension headlessly before merge. ## Key points - `tool_execution_start` is non-blocking: the extension cannot deny the call. Use `tool_call` when the extension must block a tool. It supports `{ block: true, reason }` responses. - `ctx.ui.setFooter()` is a single-slot replace API. Two extensions that register a footer collide silently because pi's conflict diagnostics cover only tools, commands, and flags. The overwritten extension keeps running with no error or log line. - Extension load order is packages first, then top-level extensions, so a local extension always wins a footer race against a package. Last write wins. - Some footer packages register a no-op placeholder footer and draw the visible bar through the editor component border. Replacing their footer registration leaves the bar stale, refreshing only on unrelated re-renders. - Status segments hide silently when their visible width is zero. Hyperlink escape sequences are excluded from width measurement, so hyperlinked segments survive the check. Powerline `customItems` can promote any extension status into the footer: `selfColorize` consumes pre-colored output, and `hideWhenMissing` hides the segment where the status is absent. - pi re-runs the extension factory on session replacement. `withSession` and the destination session's input handler never share closure state, and `pi.setModel` inside `withSession` fatals on a stale extension context (pi 0.99.1). Keep pending runtime intent at module level, keyed by session file path, and apply it in the destination's first `input` handler. - An extension reading keybindings through the package API reads a second module graph while the TUI initializes keybindings in its bundled graph. The extension's singleton is always empty, so its hint renders wrong regardless of `keybindings.json`. - Two packages that register the same tool name conflict. The duplicate can be a history constraint: a shared base carries the tool because other agents crash at launch without it. Check the history first. - `agent_end` does not mean idle. Messages sent from that event need `{deliverAs: "followUp"}`, or they fail with the `streamingBehavior` error. - Before you build a plugin that launches agents, read the installed package sources. An existing package may already own remote pane creation and agent placement. - A glob-based extension filter against a package-relative path that does not exist silently loads zero extensions. The tool never errors. Check startup output to confirm what is active. - Validate headless before merge: `pi -p` captures startup notification output, `pi -ne <ext.ts>` runs a one-shot session with only the pilot extension, and `pi --no-extensions -e <path>` loads a feature branch without it touching the live install. - Smoke-test off-repo: copy the extension to a temp dir under `$HOME` (`/tmp` breaks `node_modules` import resolution), point `PI_CODING_AGENT_DIR` at a temp agent dir, and drive the real `tool_call` handler. - Validate a footer config through the package's own parser before reload. A visual check confirms nothing. - The pi batch tool runs zsh, not bash. Re-run failing shell commands with an explicit `bash` when they exit with syntax errors. - A command-only package can run at session end. Hook `agent_before_settle` and return `continue: true`. - The TUI never enables terminal mouse tracking, so OSC 8 hyperlinks in the footer are handled natively by the terminal. Warp does not parse hyperlink escapes yet and degrades them to plain text. - When you replace a plugin, pick the one that hooks the settled event, delivers follow-up messages safely, and persists approved plans across resume and compaction. Do not keep both enabled when they register the same command. ## How to apply 1. Block a tool only from the `tool_call` handler, never from `tool_execution_start`. 2. Before you add a footer, search installed packages for `setFooter` calls. Assume one slot. 3. Keep per-session pending state at module level, keyed by session file path. Apply it in the destination session's first `input` handler. 4. Check the history before you split a duplicate tool registration. 5. Deliver messages after `agent_end` with `{deliverAs: "followUp"}`. 6. Confirm extension loads from startup output, then validate headless: `pi -p`, `pi -ne <ext.ts>`, `pi --no-extensions -e <path>`. 7. Test off-repo from a temp dir under `$HOME`, with `PI_CODING_AGENT_DIR` pointed at a temp agent dir. 8. Enable only one plugin per command name. ## Observations - [api] `setFooter` is a single slot, and footer conflicts are silent. #pi-extensions (source: agent sessions, 2026-08) - [api] The extension factory re-runs on session replacement, and closure state does not survive. #pi-extensions (source: agent sessions, 2026-10) - [api] `agent_end` is not idle, and follow-up messages need explicit delivery. #pi-extensions (source: agent sessions, 2026-08) - [testing] A headless one-shot run validates an extension before merge without touching the live install. #smoke-test (source: agent sessions, 2026-06) - [compatibility] Warp degrades OSC 8 hyperlinks to plain text. #terminal-compat (source: agent sessions, 2026-06) ## Relations - relates_to [[resources/documentation/Agents/Agents - Guards, Hooks, and Security Boundaries]] (the blocking `tool_call` event complements guards and hooks) - relates_to [[resources/documentation/Agents/Claude Code - Plugins, Settings, and Scheduled Tasks]] (plugin registration across agent CLIs) - relates_to [[resources/documentation/Agents/Agents - Parallel Orchestration and Delegation]] (an extension hands off agent dispatch instead of launching agents itself) - relates_to [[resources/documentation/Shell - Zsh and Bash Differences]] (the batch tool runs zsh, not bash) ## Sources - Personal agent-session notes, May 2026 to October 2026