How Ponytail Routes Output for Claude Code, Codex, and Copilot CLI: Platform Detection Deep Dive
Ponytail uses environment variable detection in hooks/ponytail-runtime.js to determine whether it is running inside VS Code Copilot, Claude Code (Codex), Qoder, or native Claude, then routes JSON or raw string output through writeHookOutput accordingly.
The open-source Ponytail project by DietrichGebert provides a runtime hook system that must adapt its output format to satisfy the distinct input expectations of multiple AI coding platforms. Understanding how Ponytail routes its output for Claude Code, Codex, and Copilot CLI reveals a sophisticated environment-based detection system that ensures compatibility without manual configuration.
Platform Detection Logic in ponytail-runtime.js
Ponytail’s routing mechanism centers on three boolean flags calculated at module load time in hooks/ponytail-runtime.js. These flags determine both the state directory location and the serialization strategy used by the writeHookOutput function.
Environment Variable Detection
The runtime inspects specific environment variables to identify the host platform:
const isCopilot = Boolean(process.env.COPILOT_PLUGIN_DATA) ||
isVsCodeCopilotRoot(process.env.CLAUDE_PLUGIN_ROOT);
const isCodex = !isCopilot && Boolean(process.env.PLUGIN_DATA);
const isQoder = !isCopilot && !isCodex && Boolean(process.env.QODER_SESSION_ID);
Detection priority follows a strict precedence: Copilot is checked first, followed by Codex, then Qoder. If none match, the runtime falls back to native Claude behavior. This ordering prevents misidentification when multiple environment variables are present.
Detection Criteria by Platform
| Platform | Detection Condition |
|---|---|
| VS Code Copilot | COPILOT_PLUGIN_DATA exists or CLAUDE_PLUGIN_ROOT contains both ".vscode" and "agent-plugins" |
| Claude Code (Codex) | PLUGIN_DATA exists and Copilot detection is false |
| Qoder | QODER_SESSION_ID exists and neither Copilot nor Codex detected |
| Native Claude | No special environment variables set (default fallback) |
Output Format Routing by Platform
Once detected, writeHookOutput(eventName, mode, context) branches into four distinct output strategies. Each platform receives a specifically shaped payload to match its plugin protocol expectations.
VS Code Copilot: Minimal SessionStart JSON
For Copilot, Ponytail emits output only during SessionStart events. According to the source code at lines 52-57, when isCopilot is true and the event is SessionStart, the function writes a JSON object containing additionalContext. All other events produce no output to avoid confusing the Copilot agent protocol.
// Copilot detection active
process.env.COPILOT_PLUGIN_DATA = '/tmp/copilot-data';
require('./hooks/ponytail-runtime').writeHookOutput('UserPromptSubmit', 'on', 'extra info');
// → No output (silenced for non-SessionStart events)
Claude Code / Codex: systemMessage Structure
When running as Codex (detected via PLUGIN_DATA), Ponytail outputs a JSON object containing a systemMessage field formatted as "PONYTAIL:<MODE>" and, when context is supplied, a nested hookSpecificOutput object (lines 58-67).
process.env.PLUGIN_DATA = '/tmp/codex-data';
require('./hooks/ponytail-runtime').writeHookOutput('UserPromptSubmit', 'on', 'my context');
// → {"systemMessage":"PONYTAIL:ON","hookSpecificOutput":{"hookEventName":"UserPromptSubmit","additionalContext":"my context"}}
This format allows Claude Code to interpret the mode via system message while receiving structured event data through hookSpecificOutput.
Qoder: hookSpecificOutput Only
Qoder follows the Codex pattern but omits the systemMessage wrapper (lines 69-80). When isQoder is true, writeHookOutput emits only the hookSpecificOutput object when context is present, making it compatible with Qoder's streamlined hook interface.
Native Claude: Raw stdout Fallback
In the default case (lines 82-89), Ponytail checks if the event is SubagentStart. If so, it wraps the context in hookSpecificOutput; otherwise, it writes the raw context string directly to stdout. This preserves backward compatibility with native Claude plugin handlers that expect plain text for most events.
// No special env vars → native Claude
require('./hooks/ponytail-runtime').writeHookOutput('UserPromptSubmit', 'on', 'plain text');
// → plain text written directly to stdout
Practical Implementation Examples
To invoke Ponytail correctly from a custom plugin, import writeHookOutput and pass the event name, mode, and context:
const { writeHookOutput } = require('../hooks/ponytail-runtime');
function onSessionStart(context) {
const mode = require('../hooks/ponytail-runtime').readMode() || 'off';
writeHookOutput('SessionStart', mode, context);
}
The mode value (e.g., "on" or "off") is typically read from a state file managed by ponytail-config.js, ensuring consistent behavior across the SessionStart, UserPromptSubmit, and SubagentStart hooks.
Summary
- Environment detection occurs at runtime via
COPILOT_PLUGIN_DATA,PLUGIN_DATA, andQODER_SESSION_IDchecks inhooks/ponytail-runtime.js. - Copilot receives JSON output only for
SessionStartevents, with all other events silenced to match VS Code's agent plugin expectations. - Codex receives a structured JSON payload containing
systemMessageandhookSpecificOutputfields. - Qoder receives the same
hookSpecificOutputstructure as Codex but without the enclosingsystemMessage. - Native Claude falls back to raw stdout output for most events, using
hookSpecificOutputwrapping only forSubagentStart.
Frequently Asked Questions
How does Ponytail detect Copilot versus Claude Code?
Ponytail checks process.env.COPILOT_PLUGIN_DATA first. If present, or if CLAUDE_PLUGIN_ROOT contains ".vscode" and "agent-plugins", it treats the environment as VS Code Copilot. If Copilot is not detected but process.env.PLUGIN_DATA exists, it assumes Claude Code (Codex) mode.
What output format does Codex expect from Ponytail?
Codex expects a JSON object with a systemMessage field formatted as "PONYTAIL:<MODE>" and a nested hookSpecificOutput object containing hookEventName and additionalContext keys, as implemented in lines 58-67 of hooks/ponytail-runtime.js.
Why does Copilot only receive output on SessionStart?
The VS Code Copilot agent protocol only reads additionalContext during the initial SessionStart event. To prevent protocol errors, Ponytail explicitly returns early from writeHookOutput for all other events when isCopilot is true, producing no output for UserPromptSubmit or SubagentStart.
How can I test Ponytail's platform routing locally?
Set the corresponding environment variable before requiring the runtime module. For Codex testing, assign a value to PLUGIN_DATA; for Copilot, set COPILOT_PLUGIN_DATA; for Qoder, set QODER_SESSION_ID. Then invoke writeHookOutput and inspect stdout to verify the correct JSON structure or silence for each platform.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →