18 KiB
Ponytail Plugin — Deep-dive: Claude Code a Pi Coding Agent
Zdroj: https://github.com/DietrichGebert/ponytail
Verze: 4.8.3 | Licence: MIT
Analýza vyčtená přímo ze zdrojových kódů
1. Claude Code — jak se plugin zapojuje
1.1 Manifest (.claude-plugin/plugin.json)
{
"name": "ponytail",
"version": "4.8.3",
"description": "Lazy senior dev mode...",
"author": { "name": "Dietrich Gebert", "url": "..." },
"hooks": "./hooks/claude-codex-hooks.json"
}
Jediné klíčové pole: hooks → ukazuje na JSON soubor s definicemi lifecycle hook bindings. Claude Code při načtení pluginu přečte tento manifest a zaregistruje hooky.
1.2 Hook bindings (hooks/claude-codex-hooks.json)
{
"hooks": {
"SessionStart": [
{
"matcher": "startup|resume|clear|compact",
"hooks": [
{
"type": "command",
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/ponytail-activate.js\"; exit 0",
"timeout": 5,
"statusMessage": "Loading ponytail mode..."
}
]
}
],
"SubagentStart": [
{
"hooks": [
{
"type": "command",
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/ponytail-subagent.js\"; exit 0",
"timeout": 5
}
]
}
],
"UserPromptSubmit": [
{
"hooks": [
{
"type": "command",
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/ponytail-mode-tracker.js\"; exit 0",
"timeout": 5
}
]
}
]
}
}
Tři hook body, každý spouští Node.js skript:
| Hook | Kdy se spouští | Co dělá |
|---|---|---|
SessionStart |
Při startup, resume, clear, compact |
Aktivuje mód, zapíše flag file, emituje instrukce do system promptu |
SubagentStart |
Při spuštění subagentu (Task tool) | Předá ponytail kontext subagentu |
UserPromptSubmit |
Při každém user promptu | Detekuje /ponytail commandy, přepíná mód |
Důležité: exit 0 na konci každého commandu — hook musí skončit úspěšně, jinak Claude považuje celý hook za failed.
1.3 SessionStart hook (ponytail-activate.js) — 91 řádků
Flow:
- Načte default mód z
ponytail-config.js(env var > config file >'full') - Pokud
off: vymaže flag file, vypíše'OK'(nebo prázdný string pro Codex), exit - Zapíše flag file
~/.claude/.ponytail-actives aktuálním módem - Načte instrukce z
SKILL.mdpřesponytail-instructions.js, filtruje podle módu - Detekuje statusline — kontroluje
~/.claude/settings.jsonzda existujestatusLineconfig; pokud ne, připojí do outputu nudge pro uživatele - Vypíše instrukce na stdout — Claude Code stdout zachytí a připojí k system promptu
Klíčový kód (zkráceně):
const mode = getDefaultMode(); // 'full' default
if (mode === 'off') {
clearMode();
writeHookOutput('SessionStart', 'off', isCodex ? '' : 'OK');
process.exit(0);
}
setMode(mode); // zapíše ~/.claude/.ponytail-active
let output = getPonytailInstructions(mode); // načte SKILL.md, filtruje
// Detekce chybějícího statusline
if (!hasStatusline) {
output += "\n\nSTATUSLINE SETUP NEEDED: ...";
}
writeHookOutput('SessionStart', mode, output); // → stdout → Claude system prompt
Stdout capture: Claude spustí skript jako shell command, přečte stdout a připojí obsah k system promptu jako hidden context. Uživatel to nevidí, ale LLM to vidí jako instrukce.
1.4 SubagentStart hook (ponytail-subagent.js) — 22 řádků
Proč existuje: SessionStart context je parent-thread only a nikdy nedosáhne subagentů (issue #252 v ponytail repo). Bez tohoto hooku by každý Task-spawned agent běžel bez ponytail pravidel.
Flow:
- Přečte mód z flag file (
readMode()) - Pokud
offnebo chybí flag — exit, nic neinjectuje - Načte instrukce pro daný mód
- Vypíše JSON s
hookSpecificOutput— Claude SubagentStart vyžaduje tento formát
const mode = readMode();
if (!mode || mode === 'off') process.exit(0);
writeHookOutput('SubagentStart', mode, getPonytailInstructions(mode));
Rozdíl oproti SessionStart: SubagentStart musí vracet JSON:
{"hookSpecificOutput": {"hookEventName": "SubagentStart", "additionalContext": "...instrukce..."}}
Pokud by vrátil raw text (jako SessionStart), Claude by kontext zahodil.
1.5 UserPromptSubmit hook (ponytail-mode-tracker.js) — 55 řádků
Flow:
- Čte stdin jako JSON (
{"prompt": "..."}) — Claude předává user prompt hookům přes stdin - Parsuje prompt, hledá
/ponytail [lite|full|ultra|off]nebo/ponytail-review - Při matchi zapíše/maže flag file a emituje potvrzení
- Detekuje deaktivační commandy:
"stop ponytail"nebo"normal mode"(celá zpráva, ne substring)
process.stdin.on('end', () => {
const data = JSON.parse(input.replace(/^\uFEFF/, '')); // strip BOM
const prompt = (data.prompt || '').trim().toLowerCase();
if (/^[/@$]ponytail/.test(prompt)) {
const parts = prompt.split(/\s+/);
const cmd = parts[0].replace(/^[@$]/, '/');
const arg = parts[1] || '';
if (cmd === '/ponytail-review') mode = 'review';
else if (cmd === '/ponytail') {
if (arg === 'lite') mode = 'lite';
else if (arg === 'full') mode = 'full';
else if (arg === 'ultra') mode = 'ultra';
else if (arg === 'off') mode = 'off';
else mode = getDefaultMode();
}
if (mode && mode !== 'off') {
setMode(mode);
writeHookOutput('UserPromptSubmit', mode, 'PONYTAIL MODE CHANGED — level: ' + mode);
} else if (mode === 'off') {
clearMode();
writeHookOutput('UserPromptSubmit', 'off', 'PONYTAIL MODE OFF');
}
}
});
Důležité: UserPromptSubmit hook běží před zpracováním promptu LLM. Pokud detekuje /ponytail command, zapíše flag file a emituje potvrzení — ale samotný /ponytail text pak jde do LLM jako normální prompt. LLM vidí potvrzení změny módu a reaguje na něj.
1.6 Runtime (ponytail-runtime.js) — 68 řádků
Flag file pattern:
- Cesta:
~/.claude/.ponytail-active(nebo$CLAUDE_CONFIG_DIR/.ponytail-active) - Obsah: prostý text —
'lite','full','ultra','review', nebo chybí = off
Output formáty podle runtime:
const isCopilot = Boolean(process.env.COPILOT_PLUGIN_DATA);
const isCodex = !isCopilot && Boolean(process.env.PLUGIN_DATA);
function writeHookOutput(event, mode, context) {
if (isCopilot) {
// Copilot: JSON s additionalContext na SessionStart
process.stdout.write(JSON.stringify(
event === 'SessionStart' && context ? { additionalContext: context } : {}));
return;
}
if (isCodex) {
// Codex: JSON se systemMessage + hookSpecificOutput
const output = { systemMessage: `PONYTAIL:${mode.toUpperCase()}` };
if (context) output.hookSpecificOutput = { hookEventName: event, additionalContext: context };
process.stdout.write(JSON.stringify(output));
return;
}
// Native Claude:
if (event === 'SubagentStart') {
// SubagentStart vyžaduje JSON formát
process.stdout.write(JSON.stringify(
{ hookSpecificOutput: { hookEventName: event, additionalContext: context } }));
return;
}
// SessionStart / UserPromptSubmit: raw stdout
process.stdout.write(context);
}
Detekce runtime:
PLUGIN_DATAenv var = Codex CLICOPILOT_PLUGIN_DATAenv var = GitHub Copilot- Nic z toho = native Claude Code
1.7 Instrukce builder (ponytail-instructions.js) — 94 řádků
Jedna pravda pro všechny platformy:
- Čte
skills/ponytail/SKILL.md(relativní cesta../skills/ponytail/SKILL.md) - Odstraní YAML frontmatter (
---...---) - Filtruje podle módu:
- Tabulky: řádky začínající
| **mode** |se ponechají jen pro aktivní mód - Worked examples: řádky
- mode: ...se ponechají jen pro aktivní mód - Ostatní řádky (obecná pravidla) se ponechají vždy
- Tabulky: řádky začínající
- Pro
reviewmód vrací jiný text:'PONYTAIL MODE ACTIVE — level: review. Behavior defined by /ponytail-review skill.' - Fallback: pokud
SKILL.mdneexistuje, generuje inline instrukce (getFallbackInstructions())
function getPonytailInstructions(mode) {
const configuredMode = normalizePersistedMode(mode) || DEFAULT_MODE;
if (INDEPENDENT_MODES.has(configuredMode)) { // 'review'
return 'PONYTAIL MODE ACTIVE — level: ' + configuredMode + '. Behavior defined by /ponytail-' + configuredMode + ' skill.';
}
try {
return 'PONYTAIL MODE ACTIVE — level: ' + effectiveMode + '\n\n' +
filterSkillBodyForMode(fs.readFileSync(SKILL_PATH, 'utf8'), effectiveMode);
} catch (e) {
return getFallbackInstructions(effectiveMode); // fallback když SKILL.md chybí
}
}
1.8 Konfigurace (ponytail-config.js) — 122 řádků
Resolution order default módu:
PONYTAIL_DEFAULT_MODEenv var$XDG_CONFIG_HOME/ponytail/config.jsonnebo~/.config/ponytail/config.json'full'
Validní módy: off, lite, full, ultra, review
Deaktivace: isDeactivationCommand(text) matchuje celou zprávu "stop ponytail" nebo "normal mode" (ignoruje case a trailing punctuation). Důvod: dřívější verze matchovaly substring, což deaktivovalo mód u běžných requestů jako "add a normal mode toggle".
2. Pi Coding Agent — jak se plugin zapojuje
2.1 Plugin entry point (pi-extension/index.js) — 189 řádků, ESM
import { createRequire } from "node:module";
const require = createRequire(import.meta.url);
// Import sdílených modulů z hooks/
const { getDefaultMode, normalizePersistedMode, ... } = require("../hooks/ponytail-config.js");
const { getPonytailInstructions, filterSkillBodyForMode } = require("../hooks/ponytail-instructions.js");
export default function ponytailExtension(pi) {
let currentMode = DEFAULT_MODE;
let configuredDefaultMode = getDefaultMode();
let isActive = false;
let lastCtx = null;
// ...
}
Pi používá stejné sdílené moduly jako Claude (ponytail-config.js, ponytail-instructions.js) — jedna pravda pro obě platformy.
2.2 Command registrace
Pi má explicitní API pro commandy — nikoli hooky jako Claude:
pi.registerCommand("ponytail", {
description: "Set or report Ponytail mode",
handler: async (args, ctx) => {
const parsed = parsePonytailCommand(args, configuredDefaultMode);
if (parsed.type === "status") {
ctx?.ui?.notify?.(`Ponytail: current ${currentMode} • default ${configuredDefaultMode}`, "info");
return;
}
if (parsed.type === "set-default") {
writeDefaultMode(parsed.mode); // zapíše do ~/.config/ponytail/config.json
configuredDefaultMode = getDefaultMode();
return;
}
if (parsed.type === "set-mode") {
setMode(parsed.mode, ctx); // změní currentMode, zapíše do session entries
return;
}
}
});
Podporované argumenty:
/ponytail→ toggle (off → full, jinak default)/ponytail lite|full|ultra|off→ set mód/ponytail status→ zobrazí current + default/ponytail default <mode>→ nastaví default mód do config file
Skill aliasy:
pi.registerCommand("ponytail-review", {
handler: (_args, ctx) => sendAlias("/skill:ponytail-review", "", ctx)
});
// + ponytail-audit, ponytail-gain, ponytail-debt, ponytail-help
sendAlias() posílá zprávu jako user message — buď okamžitě (pokud agent idle) nebo jako follow-up (pokud agent běží).
2.3 Event hooks (pi.on())
Pi používá event-driven API místo Claude lifecycle hooks:
| Event | Kdy se spouští | Co dělá |
|---|---|---|
input |
Při každém user inputu | Detekuje "stop ponytail" / "normal mode", deaktivuje |
session_start |
Při startu session | Načte mód z session historie nebo defaultu, sync status bar |
agent_start |
Když agent začne pracovat | isActive = true, sync status bar |
agent_end |
Když agent skončí | isActive = false, sync status bar |
before_agent_start |
Před LLM voláním | Injectuje instrukce do systemPrompt |
Klíčový hook — before_agent_start:
pi.on("before_agent_start", async (event) => {
if (!currentMode || currentMode === "off") return;
return { systemPrompt: `${event.systemPrompt}\n\n${getPonytailInstructions(currentMode)}` };
});
Rozdíl oproti Claude: Pi explicitně modifikuje systemPrompt a vrací ho jako return value. Claude zachytává stdout a připojuje ho interně.
2.4 Persistencia módu v Pi
Pi nepoužívá flag file jako Claude. Místo toho:
- Session entries:
pi.appendEntry("ponytail-mode", { mode: normalized })— zapisuje mód do session logu - Session restore:
resolveSessionMode(entries, fallback)— přisession_startčte historii entries typu"ponytail-mode"od konce a najde poslední uložený mód - Default mód:
getDefaultMode()ze sdíleného configu (env var >~/.config/ponytail/config.json>'full')
export function resolveSessionMode(entries, fallbackMode = DEFAULT_MODE) {
if (!Array.isArray(entries)) return fallback;
for (let i = entries.length - 1; i >= 0; i -= 1) {
const entry = entries[i];
if (entry?.type !== "custom" || entry?.customType !== "ponytail-mode") continue;
const mode = normalizePersistedMode(entry?.data?.mode);
if (mode) return mode;
}
return fallback;
}
Proč ne flag file? Pi agent má vlastní session management (sessionManager.getBranch(), sessionManager.getEntries()). Plugin využívá nativní Pi API místo externího souboru.
2.5 Status bar
function syncStatus(ctx) {
if (!c?.ui?.setStatus || !c.ui.theme?.fg) return;
const levelIcons = { lite: "🌿", full: "⚡", ultra: "🔥" };
const icon = levelIcons[currentMode] || "";
const label = currentMode.toUpperCase();
const indicator = isActive ? theme.fg("accent", "●") : theme.fg("dim", "○");
c.ui.setStatus("ponytail", indicator + " 🐴 " + theme.fg("muted", "ponytail: ") + theme.fg("text", icon + " " + label));
}
Pi má nativní status bar API (ui.setStatus). Claude nemá — proto potřebuje externí bash skript (ponytail-statusline.sh) který se musí ručně nakonfigurovat v settings.json.
3. Srovnání: Claude Code vs Pi
| Aspekt | Claude Code | Pi Coding Agent |
|---|---|---|
| Plugin API | Manifest JSON + lifecycle hooks | pi.registerCommand() + pi.on() events |
| Prompt injection | Stdout capture — skript píše na stdout, Claude připojí k system promptu | before_agent_start event — explicitní modifikace systemPrompt |
| Subagent support | SubagentStart hook — explicitní předání kontextu |
Závisí na Pi interním chování (není explicitní SubagentStart) |
| Command parsing | UserPromptSubmit hook parsuje stdin JSON, detekuje /ponytail |
pi.registerCommand("ponytail", ...) — nativní command API |
| Persistencia módu | Flag file ~/.claude/.ponytail-active |
Session entries (pi.appendEntry) + config file |
| Status indikace | Externí bash skript (ponytail-statusline.sh) konfigurovaný v settings.json |
Nativní ui.setStatus() API |
| Deaktivace | "stop ponytail" / "normal mode" via UserPromptSubmit hook |
pi.on("input", ...) detekuje stejné fráze |
| Output formát | Raw stdout (SessionStart) nebo JSON (SubagentStart) | Return value { systemPrompt: "..." } |
| Runtime detekce | Env vars (PLUGIN_DATA, COPILOT_PLUGIN_DATA) |
Není potřeba — Pi má vlastní API |
| Sdílené kódy | ponytail-config.js, ponytail-instructions.js |
Stejné + ponytail-config.js, ponytail-instructions.js |
4. Klíčové technické detaily
4.1 Stdout vs explicitní API
Claude: Hook skripty jsou externí procesy. Claude spustí node script.js, přečte stdout a interně připojí k promptu. Skript nemá přímý přístup k Claude interním strukturám.
Pi: Plugin běží v rámci Pi procesu jako ESM modul. Má přímý přístup k pi objektu a může volat pi.on(), pi.registerCommand(), modifikovat systemPrompt.
4.2 Flag file vs session entries
Claude: Používá souborový flag (~/.claude/.ponytail-active) protože:
- Hook skripty běží jako samostatné procesy — nemají přístup ke Claude session state
- Flag file je jediný způsob, jak předat stav mezi SessionStart a SubagentStart hooky
- Jednoduché, robustní, nezávislé na DB
Pi: Používá session entries (pi.appendEntry) protože:
- Pi má nativní session management API
- Entries se persistují v rámci session a jsou dostupné při restore
- Lepší integrace s Pi architekturou
4.3 BOM handling
Oba systémy řeší UTF-8 BOM (Byte Order Mark) který některé editory přidávají na začátek souborů:
// Claude: v mode-tracker.js (stdin JSON)
const data = JSON.parse(input.replace(/^\uFEFF/, ''));
// Claude: v activate.js (settings.json)
const raw = fs.readFileSync(settingsPath, 'utf8').replace(/^\uFEFF/, '');
BOM breakuje JSON.parse() — proto se explicitně stripuje.
4.4 Silent fail philosophy
Celý plugin je navržený s silent fail přístupem:
try {
setMode(mode);
} catch (e) {
// Silent fail -- flag is best-effort, don't block the hook
}
try {
writeHookOutput('SessionStart', mode, output);
} catch (e) {
// Silent fail — stdout closed/EPIPE at hook exit must not surface as a hook failure
}
Důvod: hook failure by zastavila celou session. Plugin je best-effort — když selže, Claude běží normálně bez ponytail.
4.5 Shell safety
function isShellSafe(p) {
return typeof p === 'string' && /^[A-Za-z0-9 _.\-:/\\~]+$/.test(p);
}
Před vložením cesty pluginu do shell commandu (statusline config) se kontroluje, že cesta neobsahuje shell metacharacters. Pokud ano, nudge se změní na manuální setup místo embeddovaného command snippetu.