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_DATAas the state directory for the.ponytail-activeflag, 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
additionalContextfor theSessionStartevent, providing Copilot with contextual hints - Native Claude Code: Emits raw stdout or full JSON responses including
hookSpecificOutputforSubagentStartevents
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_DATAand parsingprocess.env.CLAUDE_PLUGIN_ROOTvia theisVsCodeCopilotRoot()helper. - The
isVsCodeCopilotRoot()function specifically looks for theagent-pluginspath segment combined with.vscodeto 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.jswith configuration helpers available inhooks/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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →