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, and QODER_SESSION_ID checks in hooks/ponytail-runtime.js.
  • Copilot receives JSON output only for SessionStart events, with all other events silenced to match VS Code's agent plugin expectations.
  • Codex receives a structured JSON payload containing systemMessage and hookSpecificOutput fields.
  • Qoder receives the same hookSpecificOutput structure as Codex but without the enclosing systemMessage.
  • Native Claude falls back to raw stdout output for most events, using hookSpecificOutput wrapping only for SubagentStart.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →