# How the Hook Output JSON Shape Differs Between Native Claude Code, Codex, and Qoder

> Explore how hook output JSON shapes vary across Native Claude Code, Codex, and Qoder. Understand the unique requirements for systemMessage and raw text output in this detailed comparison.

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

---

**The `writeHookOutput` function in Ponytail emits three distinct JSON schemas depending on whether it detects Native Claude Code, Claude Code Codex, or Qoder, with only Codex requiring a `systemMessage` field and only Native Claude supporting raw text output for non-subagent events.**

The **[DietrichGebert/ponytail](https://github.com/DietrichGebert/ponytail)** repository provides a portable hook system that adapts its **hook output JSON shape** to the specific LLM host environment. Understanding these differences is critical when debugging context passing or extending the plugin across different AI coding agents.

## Environment Detection Logic

Inside **[`hooks/ponytail-runtime.js`](https://github.com/DietrichGebert/ponytail/blob/main/hooks/ponytail-runtime.js)**, the `writeHookOutput` function determines the target environment by checking for specific environment variables at runtime. The detection follows a priority order:

- **Codex**: Identified by the presence of `PLUGIN_DATA` (setting `isCodex` to true)
- **Qoder**: Identified by the presence of `QODER_SESSION_ID` (setting `isQoder` to true)  
- **Native Claude Code**: Default fallback when neither Copilot, Codex, nor Qoder variables are present

This detection logic resides in the conditional branches spanning lines **58–89** of the runtime file.

## JSON Output Shapes by Environment

Each environment expects a different payload structure to correctly surface additional context to the user or sub-agent.

### Native Claude Code

When running in the native Anthropic CLI tool (no special environment variables set), the function produces **two possible output shapes** depending on the event type.

For **`SubagentStart`** events (lines **82–89**), the output wraps context in a structured object:

```json
{
  "hookSpecificOutput": {
    "hookEventName": "SubagentStart",
    "additionalContext": "<text>"
  }
}

```

For **all other events**, the function bypasses JSON serialization entirely and emits the raw text string directly to stdout. This allows Native Claude to consume plain context without additional parsing overhead.

### Claude Code Codex

When the Codex plugin is active (`isCodex === true`), the output always includes a **`systemMessage`** field that prefixes the current Ponytail mode. According to lines **58–66**, the structure is:

```json
{
  "systemMessage": "PONYTAIL:<MODE>",
  "hookSpecificOutput": {
    "hookEventName": "<event>",
    "additionalContext": "<text>"
  }
}

```

The `hookSpecificOutput` block is appended only when `additionalContext` is non-empty. The **systemMessage** serves as a mode banner that Codex renders in the conversation interface to indicate whether the plugin is operating in `lite`, `full`, or another mode.

### Qoder

Qoder sessions (`isQoder === true`) expect a simplified structure that omits the `systemMessage` entirely. As implemented in lines **69–79**, the output contains only the **`hookSpecificOutput`** wrapper:

```json
{
  "hookSpecificOutput": {
    "hookEventName": "<event>",
    "additionalContext": "<text>"
  }
}

```

Qoder injects this context directly into the agent's conversation stream without requiring the mode prefix, relying instead on internal session state tracked via `QODER_SESSION_ID`.

## Key Structural Differences

Three critical distinctions separate the implementations:

- **Presence of `systemMessage`**: Only Codex requires this field to display the mode banner. Qoder and Native Claude omit it entirely.
- **Event-specific handling**: Native Claude treats `SubagentStart` as a special case requiring JSON wrapping, while emitting raw text for all other events. Both Codex and Qoder consistently wrap output in `hookSpecificOutput` objects when context is present.
- **Raw text vs. JSON**: Native Claude is the only environment that outputs unwrapped plain text for standard events, whereas Codex and Qoder always produce valid JSON objects.

## Practical Code Examples

### Emitting for Codex with Mode Context

```javascript
// PLUGIN_DATA is present (isCodex === true)
writeHookOutput('UserPromptSubmit', 'full', 'Review this PR');

```

**Output:**

```json
{
  "systemMessage": "PONYTAIL:FULL",
  "hookSpecificOutput": {
    "hookEventName": "UserPromptSubmit",
    "additionalContext": "Review this PR"
  }
}

```

### Emitting for Qoder

```javascript
// QODER_SESSION_ID is present (isQoder === true)
writeHookOutput('UserPromptSubmit', 'full', 'Review this PR');

```

**Output:**

```json
{
  "hookSpecificOutput": {
    "hookEventName": "UserPromptSubmit",
    "additionalContext": "Review this PR"
  }
}

```

### Native Claude SubagentStart Event

```javascript
// No environment variables set (native mode)
writeHookOutput('SubagentStart', 'lite', 'Initialize sub-agent');

```

**Output:**

```json
{
  "hookSpecificOutput": {
    "hookEventName": "SubagentStart",
    "additionalContext": "Initialize sub-agent"
  }
}

```

### Native Claude Standard Event

```javascript
writeHookOutput('UserPromptSubmit', 'lite', 'Just a plain text note');

```

**Output:**

```

Just a plain text note

```

## Implementation Reference

The core logic resides in **[`hooks/ponytail-runtime.js`](https://github.com/DietrichGebert/ponytail/blob/main/hooks/ponytail-runtime.js)**, which defines the `writeHookOutput` function and environment detection heuristics. The function interacts with **[`hooks/ponytail-mode-tracker.js`](https://github.com/DietrichGebert/ponytail/blob/main/hooks/ponytail-mode-tracker.js)** to persist and retrieve the active mode before serializing output. These divergent JSON shapes ensure compatibility with each host's specific context consumption protocol—Codex requires explicit mode signaling via `systemMessage`, Qoder expects minimal JSON payloads, and Native Claude optimizes for raw text efficiency except when initializing subagents.

## Summary

- **Codex** requires a `systemMessage` field containing `"PONYTAIL:<MODE>"` plus a conditional `hookSpecificOutput` object.
- **Qoder** accepts only the `hookSpecificOutput` wrapper without any `systemMessage` field.
- **Native Claude Code** emits raw text for most events but wraps `SubagentStart` events in a `hookSpecificOutput` object.
- All three formats are generated by `writeHookOutput` in [`hooks/ponytail-runtime.js`](https://github.com/DietrichGebert/ponytail/blob/main/hooks/ponytail-runtime.js) based on environment variable detection.

## Frequently Asked Questions

### Why does Codex require a systemMessage field while Qoder does not?

Codex uses the `systemMessage` to render a visual mode banner in the conversation interface, indicating whether Ponytail is operating in `lite`, `full`, or another mode. Qoder manages mode state internally through the `QODER_SESSION_ID` and injects context directly into the conversation stream without requiring an explicit banner field.

### What happens if additionalContext is empty when emitting for Codex?

According to the implementation in [`hooks/ponytail-runtime.js`](https://github.com/DietrichGebert/ponytail/blob/main/hooks/ponytail-runtime.js) lines **58–66**, the `hookSpecificOutput` block is added only when `additionalContext` is non-empty. If the context string is empty, Codex receives only the `systemMessage` field containing the mode prefix, without the nested hook event object.

### Why does Native Claude Code use raw text instead of JSON for most events?

Native Claude can accept raw text directly into the conversation buffer without JSON parsing overhead. However, the `SubagentStart` event requires the structured `hookSpecificOutput` wrapper to ensure the context persists across the subagent boundary without being stripped by the CLI's text processing pipeline.

### How does the hook detect which environment it's running in?

The `writeHookOutput` function checks for the presence of `PLUGIN_DATA` (indicating Codex) or `QODER_SESSION_ID` (indicating Qoder) environment variables. If neither is present—and no Copilot variables are detected—it defaults to Native Claude Code behavior, as defined in the conditional logic spanning lines **58–89** of [`hooks/ponytail-runtime.js`](https://github.com/DietrichGebert/ponytail/blob/main/hooks/ponytail-runtime.js).