How Ponytail Distinguishes Native Claude Code from VS Code Copilot

Ponytail checks for the COPILOT_PLUGIN_DATA environment variable and parses CLAUDE_PLUGIN_ROOT to determine if it is running inside VS Code Copilot or native Claude Code.

The DietrichGebert/ponytail repository provides a plugin that must adapt its behavior depending on whether it executes within the native Claude Code CLI or the VS Code Copilot extension. This distinction is critical because each host expects different state directories, hook output formats, and session management protocols. The detection logic resides primarily in hooks/ponytail-runtime.js and relies on environment variables injected by the host during initialization.

Environment Variable Detection

Ponytail employs two primary environment variables to distinguish native Claude Code from VS Code Copilot. The plugin inspects these variables at runtime to set an internal isCopilot flag that governs downstream behavior.

The COPILOT_PLUGIN_DATA Flag

VS Code Copilot injects a unique environment variable named COPILOT_PLUGIN_DATA, which points to the Copilot plugin’s data directory. If this variable is defined, Ponytail immediately knows it is operating under the Copilot extension. This is the most direct detection mechanism available.

Parsing CLAUDE_PLUGIN_ROOT

Both native Claude Code and VS Code Copilot set the CLAUDE_PLUGIN_ROOT environment variable, but the path structure differs between hosts:

  • VS Code Copilot: The path contains the .vscode/agent-plugins/ folder segment
  • Native Claude Code: The path points to a standard Claude plugin location (e.g., ~/.claude/plugins/…)

Detection Algorithm Implementation

The core detection logic uses a helper function isVsCodeCopilotRoot() to analyze the CLAUDE_PLUGIN_ROOT path. This function checks whether the path includes the segment agent-plugins and contains the string .vscode (case-insensitive).

// hooks/ponytail-runtime.js
function isVsCodeCopilotRoot(pluginRoot) {
  if (!pluginRoot) return false;
  return pluginRoot.split(/[\\/]+/).includes('agent-plugins') &&
    pluginRoot.toLowerCase().includes('.vscode');
}

const isCopilot = Boolean(process.env.COPILOT_PLUGIN_DATA) ||
  isVsCodeCopilotRoot(process.env.CLAUDE_PLUGIN_ROOT);

If either COPILOT_PLUGIN_DATA exists or isVsCodeCopilotRoot() returns true, the runtime sets isCopilot = true. Otherwise, the plugin assumes it is running in native Claude Code (or potentially other hosts like Codex or Qoder, which are detected via additional environment variables).

State Directory Resolution

Based on the detection result, Ponytail selects appropriate directories for storing state files:

// hooks/ponytail-runtime.js (excerpt)
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);

let stateDir = getClaudeDir();               // default = native Claude
if (isCodex)   stateDir = process.env.PLUGIN_DATA;
if (isCopilot) stateDir = process.env.COPILOT_PLUGIN_DATA || getClaudeDir();
if (isQoder)   stateDir = path.join(os.homedir(), '.qoder');

Host-Specific Runtime Behaviors

Once the plugin distinguishes native Claude Code from VS Code Copilot, it adapts three key behaviors:

State Directory for Session Flags

  • Copilot: Uses process.env.COPILOT_PLUGIN_DATA as the state directory for the .ponytail-active flag, falling back to the Claude directory only if undefined
  • Native Claude Code: Uses the standard Claude configuration directory returned by getClaudeDir()

Hook Output Format

The writeHookOutput() function in hooks/ponytail-runtime.js emits different JSON structures depending on the host:

function writeHookOutput(event, mode, context = '') {
  if (isCopilot) {
    // Copilot only cares about `additionalContext` on SessionStart
    process.stdout.write(JSON.stringify(
      event === 'SessionStart' && context ? { additionalContext: context } : {}));
    return;
  }
  // Native Claude Code emits raw stdout or full JSON with hookSpecificOutput
}
  • Copilot: Emits only additionalContext for the SessionStart event, providing Copilot with contextual hints
  • Native Claude Code: Emits raw stdout or full JSON responses including hookSpecificOutput for SubagentStart events

Status-Line Nudging

  • Copilot: Status-line nudging is disabled because Copilot never reads the nudge output
  • Native Claude Code: Status-line nudging is enabled to provide real-time feedback to Claude Code users

Summary

  • Ponytail distinguishes native Claude Code from VS Code Copilot by checking process.env.COPILOT_PLUGIN_DATA and parsing process.env.CLAUDE_PLUGIN_ROOT via the isVsCodeCopilotRoot() helper.
  • The isVsCodeCopilotRoot() function specifically looks for the agent-plugins path segment combined with .vscode to identify Copilot environments.
  • Detection affects critical behaviors including state directory selection, hook output JSON structure, and UI nudging capabilities.
  • All detection logic is centralized in hooks/ponytail-runtime.js with configuration helpers available in hooks/ponytail-config.js.

Frequently Asked Questions

How reliable is environment variable detection for distinguishing Claude Code hosts?

Environment variable detection is highly reliable because both native Claude Code and VS Code Copilot explicitly set distinct variables (CLAUDE_PLUGIN_ROOT and COPILOT_PLUGIN_DATA) during plugin initialization. These variables are injected by the host process before Ponytail executes, making them trustworthy indicators of the runtime environment.

Can Ponytail detect other AI coding assistants besides Claude Code and Copilot?

Yes, the plugin includes detection for additional hosts. After ruling out Copilot, Ponytail checks for process.env.PLUGIN_DATA to identify Codex environments, and process.env.QODER_SESSION_ID to identify Qoder sessions. Each host receives customized state directory assignments and output formatting appropriate to its protocol.

Why does Copilot require different hook output than native Claude Code?

VS Code Copilot expects a specific JSON structure containing only additionalContext during SessionStart events to provide session-wide context hints. Native Claude Code, conversely, supports richer hook outputs including hookSpecificOutput and raw stdout streams for subagent communication. Ponytail branches its writeHookOutput() function to satisfy these distinct host expectations.

What happens if neither COPILOT_PLUGIN_DATA nor CLAUDE_PLUGIN_ROOT is set?

If neither environment variable is defined, isVsCodeCopilotRoot() returns false due to the null check, and isCopilot evaluates to false. Consequently, Ponytail defaults to native Claude Code behavior, using getClaudeDir() for state storage and enabling full hook output capabilities. This fallback ensures the plugin remains functional in standard Claude Code installations even if specific variables are missing.

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 →