# How Ponytail Routes Output for Claude Code, Codex, and Copilot CLI: Platform Detection Deep Dive

> Discover how Ponytail routes output for Claude Code, Codex, and Copilot CLI. Learn about platform detection and output handling in this deep dive.

- Repository: [DietrichGebert/ponytail](https://github.com/DietrichGebert/ponytail)
- Tags: deep-dive
- Published: 2026-08-30

---

**Ponytail uses environment variable detection in [`hooks/ponytail-runtime.js`](https://github.com/DietrichGebert/ponytail/blob/main/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`](https://github.com/DietrichGebert/ponytail/blob/main/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:

```javascript
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.

```javascript
// 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).

```javascript
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.

```javascript
// 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:

```javascript
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`](https://github.com/DietrichGebert/ponytail/blob/main/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`](https://github.com/DietrichGebert/ponytail/blob/main/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`](https://github.com/DietrichGebert/ponytail/blob/main/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.