Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 18 additions & 2 deletions hooks/ponytail-runtime.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,28 @@ const path = require('path');
const { getClaudeDir } = require('./ponytail-config');

const STATE_FILE = '.ponytail-active';
const isCopilot = Boolean(process.env.COPILOT_PLUGIN_DATA);

// VS Code Copilot's agent-plugin host never sets COPILOT_PLUGIN_DATA — it only
// injects CLAUDE_PLUGIN_ROOT, pointed at its own install dir under
// <home>/.vscode/agent-plugins/... . Without this fallback, ponytail mistakes
// it for native Claude Code (issue #528): wrong hook output shape, and a
// statusLine setup nudge for a settings.json VS Code Copilot never reads.
function looksLikeVSCodeCopilotPluginRoot(pluginRoot) {
if (!pluginRoot) return false;
const segments = pluginRoot.split(/[\\/]+/);
return segments.includes('agent-plugins') &&
segments.some((s) => s.toLowerCase() === '.vscode');
}

const isCopilot = Boolean(process.env.COPILOT_PLUGIN_DATA) ||
looksLikeVSCodeCopilotPluginRoot(process.env.CLAUDE_PLUGIN_ROOT);
const isCodex = !isCopilot && Boolean(process.env.PLUGIN_DATA);

let stateDir = getClaudeDir();
if (isCodex) stateDir = process.env.PLUGIN_DATA;
if (isCopilot) stateDir = process.env.COPILOT_PLUGIN_DATA;
// Only redirect state into COPILOT_PLUGIN_DATA when it's actually set — the
// VS Code Copilot fallback above detects Copilot without it being present.
if (process.env.COPILOT_PLUGIN_DATA) stateDir = process.env.COPILOT_PLUGIN_DATA;

const statePath = path.join(stateDir, STATE_FILE);

Expand Down
34 changes: 34 additions & 0 deletions tests/hooks.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,40 @@ assert.equal(
output = JSON.parse(result.stdout);
assert.deepEqual(output, {});

// VS Code Copilot's agent-plugin host never sets COPILOT_PLUGIN_DATA — only
// CLAUDE_PLUGIN_ROOT, pointed at its own install dir (issue #528). Without
// detecting that, ponytail treats it as native Claude Code: writeHookOutput
// writes raw text instead of JSON (so JSON.parse below would throw), and the
// statusLine setup nudge fires for a settings.json Copilot never reads.
const vscodeCopilotHome = path.join(temp, 'vscode-copilot-home');
fs.mkdirSync(vscodeCopilotHome, { recursive: true });
const vscodeCopilotPluginRoot = path.join(
vscodeCopilotHome, '.vscode', 'agent-plugins', 'github.com', 'DietrichGebert', 'ponytail',
);
result = run('ponytail-activate.js', {
HOME: vscodeCopilotHome,
USERPROFILE: vscodeCopilotHome,
CLAUDE_PLUGIN_ROOT: vscodeCopilotPluginRoot,
PONYTAIL_DEFAULT_MODE: 'full',
});
assert.equal(result.status, 0, result.stderr);
assert.equal(
fs.readFileSync(path.join(vscodeCopilotHome, '.claude', '.ponytail-active'), 'utf8'),
'full',
'without COPILOT_PLUGIN_DATA, state must still land in the ~/.claude fallback',
);
output = JSON.parse(result.stdout);
assert.ok(
!('systemMessage' in output),
'must use the Copilot output shape (additionalContext only), not the Codex systemMessage shape',
);
assert.match(output.additionalContext, /PONYTAIL MODE ACTIVE — level: full/);
assert.doesNotMatch(
output.additionalContext,
/STATUSLINE SETUP NEEDED/,
'VS Code Copilot never reads settings.json statusLine — must not nudge for it',
);

// SubagentStart hook: when ponytail mode is active it injects the ruleset into
// each subagent (issue #252). Native Claude must get the hookSpecificOutput JSON
// form, not raw stdout, or the context is dropped.
Expand Down