Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
cba08b4
AX-1694 - Align VS Code copilot-instructions with Claude/Cursor agent…
MatanEden1 Jun 15, 2026
5f4db9a
AX-1694 - Apply review feedback: generalize installed-state read, mir…
MatanEden1 Jun 15, 2026
e722a81
AX-1694 - Restore type:"http" qualifier on Step 5 OAuth condition
MatanEden1 Jun 15, 2026
a033dca
Refine copilot instructions: clarify project switching and input hand…
MatanEden1 Jun 15, 2026
0606423
AX-1694 - Apply review feedback for cross-plugin alignment
MatanEden1 Jun 16, 2026
6a626ac
AX-1694 - Replace platform-specific scripts with cross-platform Node.…
MatanEden1 Jun 16, 2026
3585b81
AX-1694 - Implement full authentication precedence in inject-instruct…
MatanEden1 Jun 16, 2026
e344389
AX-1694 - Apply review feedback to inject-instructions
MatanEden1 Jun 16, 2026
5cc4d1d
AX-1694 - Clarify --login requires full permissions, not just network
MatanEden1 Jun 16, 2026
2c199e5
AX-1694 - Add jf-cli config fallback and refine MCP instructions
MatanEden1 Jun 17, 2026
01652bf
Clarify MCP config scope, server ID, and paths
MatanEden1 Jun 17, 2026
e247621
Refine MCP configuration instructions for user-level and workspace se…
MatanEden1 Jun 17, 2026
2fc87e1
Standardize MCP config on user-level ~/.vscode/mcp.json
MatanEden1 Jun 17, 2026
af832bb
Use the real VS Code MCP config locations
MatanEden1 Jun 17, 2026
ba067fa
Clarify user-level MCP config path
MatanEden1 Jun 17, 2026
dace1a6
Materialize Copilot instructions and change output
MatanEden1 Jun 17, 2026
5036f0d
Update copilot-instructions.md
MatanEden1 Jun 17, 2026
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
13 changes: 8 additions & 5 deletions plugin/hooks/hooks.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
{
"hooks": {
"sessionStart": [
"SessionStart": [
{
"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/ensure-instructions.sh",
"windows": "powershell -ExecutionPolicy Bypass -File ${CLAUDE_PLUGIN_ROOT}/scripts/ensure-instructions.ps1"
"hooks": [
{
"type": "command",
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/inject-instructions.mjs\""
}
]
}
]
}
}
}
15 changes: 0 additions & 15 deletions plugin/scripts/ensure-instructions.ps1

This file was deleted.

16 changes: 0 additions & 16 deletions plugin/scripts/ensure-instructions.sh

This file was deleted.

167 changes: 167 additions & 0 deletions plugin/scripts/inject-instructions.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
#!/usr/bin/env node
// Copyright (c) JFrog Ltd. 2026
// Licensed under the Apache License, Version 2.0
// https://www.apache.org/licenses/LICENSE-2.0

import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
import os from "node:os";
import path from "node:path";
import process from "node:process";
import { fileURLToPath } from "node:url";

// Logs go to stderr; stdout is reserved for the hook JSON payload.
const debugEnabled = process.env.JF_AGENT_GUARD_DEBUG === "true";
const log = (message) => console.error(`[jfrog-agent-guard] ${message}`);
const debug = (message) => {
if (debugEnabled) log(message);
};

// New JFROG_* env vars take precedence over the legacy JF_* names.
const env = (newName, oldName) =>
process.env[newName] ?? process.env[oldName];

const forceDisabled =
env("_JF_AGENT_GUARD_FORCE_DISABLE") === "true";
const forceEnabled =
env("JF_AGENT_GUARD_FORCE_ENABLE") === "true";

// Resolve {baseUrl, token}, preferring env vars and falling back to the JF CLI
// config (~/.jfrog/jfrog-cli.conf.v6): the profile marked isDefault, or the
// only profile when exactly one is defined. Returns null when nothing resolves.
function resolveCredentials() {
const baseUrl = env("JFROG_URL", "JF_URL");
const token = env("JFROG_ACCESS_TOKEN", "JF_ACCESS_TOKEN");
if (baseUrl && token) {
debug("Resolved credentials from environment variables");
return { baseUrl, token };
}

const confPath = path.join(os.homedir(), ".jfrog", "jfrog-cli.conf.v6");
let conf;
try {
conf = JSON.parse(readFileSync(confPath, "utf8"));
} catch (error) {
debug(`Could not read or parse JF CLI config at ${confPath}: ${error.message}`);
return null;
}

// Only profiles that actually carry a URL and access token are usable.
const servers = Array.isArray(conf?.servers)
? conf.servers.filter((s) => s.url && s.accessToken)
: [];
if (servers.length === 0) {
debug("No usable server profiles found in JF CLI config");
return null;
}

const defaultProfile = servers.find((s) => s.isDefault);
if (defaultProfile) {
debug(`Resolved credentials using default profile: ${defaultProfile.serverId}`);
return { baseUrl: defaultProfile.url, token: defaultProfile.accessToken };
}

if (servers.length === 1) {
debug(`Resolved credentials using the single available profile: ${servers[0].serverId}`);
return { baseUrl: servers[0].url, token: servers[0].accessToken };
}

debug("Multiple JF CLI profiles exist but none is marked default; cannot resolve credentials");
return null;
}

async function isAgentGuardEnabledViaSettings() {
const credentials = resolveCredentials();
if (!credentials) {
debug("No JFrog credentials resolved; skipping settings check");
return false;
}
const { baseUrl, token } = credentials;

const url =
baseUrl.replace(/\/+$/, "") +
"/ml/core/api/v1/administration/account-settings/mcp_gateway_plugin_enabled";

debug(`Fetching agent guard setting from ${url}`);

const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 5000);
try {
const response = await fetch(url, {
method: "GET",
headers: {
Accept: "application/json",
Authorization: `Bearer ${token}`,
},
signal: controller.signal,
});
if (!response.ok) {
const body = await response.text().catch(() => "");
debug(`Settings request returned HTTP ${response.status}; body: ${body || "<empty>"}`);
return false;
}
const data = await response.json();
const enabled = data?.settings?.mcpGatewayPluginEnabled?.value === true;
debug(`Settings response indicates agent guard enabled=${enabled}`);
return enabled;
} catch (error) {
const reason = error?.name === "AbortError" ? "timeout" : error?.message ?? "unknown error";
debug(`Settings request failed: ${reason}`);
return false;
} finally {
clearTimeout(timeout);
}
}

if (forceDisabled) {
debug("Force-disable flag is set.");
process.stdout.write("{}");
process.exit(0);
} else if (forceEnabled) {
debug("Force-enable flag is set.");
} else if (!(await isAgentGuardEnabledViaSettings())) {
debug("Agent Guard not enabled; exiting without injecting instructions");
process.stdout.write("{}");
process.exit(0);
}
debug("Injecting instructions");

const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");

let template;
try {
template = readFileSync(
path.join(root, "templates", "copilot-instructions.md"),
"utf8",
);
} catch (error) {
debug(`Could not read instructions template: ${error.message}`);
process.exit(0);
}
Comment on lines +131 to +139

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Critical — wrong template filename; the feature silently injects nothing. This reads templates/jfrog-mcp-management.md, but the only template in this repo is templates/copilot-instructions.md (jfrog-mcp-management.md is the Claude/Cursor filename — this looks copy-pasted from the Claude plugin). Because the read is wrapped in catch { process.exit(0) }, the failure is silent: the read throws, the script exits 0, and no instructions are ever injected. Change line 170 to "copilot-instructions.md".

Comment on lines +130 to +139

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking — wrong template filename (regression). This reads templates/jfrog-mcp-management.md, but that file does not exist in this repo — the only template is templates/copilot-instructions.md (I verified the repo tree at this commit). jfrog-mcp-management.md is the Claude/Cursor filename; this whole file looks copied from the Cursor injector. The read throws, the catch does process.stdout.write("{}") + exit(0), so the failure is silent and nothing is injected. This was fixed last round (head 5cc4d1d) and has now been reintroduced. Change line 133 back to "copilot-instructions.md".


// Materialize the template into the workspace at .github/copilot-instructions.md,
// which is the file VS Code / GitHub Copilot actually reads. This mirrors the
// legacy ensure-instructions scripts and is the primary delivery path for
// Copilot; the additionalContext payload below additionally covers Claude Code
// sessions. Only write when absent so we never clobber a user-edited file.
try {
const targetDir = path.join(process.cwd(), ".github");
const targetFile = path.join(targetDir, "copilot-instructions.md");
if (!existsSync(targetFile)) {
mkdirSync(targetDir, { recursive: true });
writeFileSync(targetFile, template, "utf8");
debug(`Wrote instructions to ${targetFile}`);
} else {
debug(`Instructions already present at ${targetFile}; leaving as-is`);
}
} catch (error) {
debug(`Failed to write .github/copilot-instructions.md: ${error.message}`);
}

process.stdout.write(
JSON.stringify({
hookSpecificOutput: {
hookEventName: "SessionStart",
additionalContext: template,
},
}),
);
Comment on lines +160 to +167

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Major — this no longer writes .github/copilot-instructions.md. The old ensure-instructions.sh copied the template into .github/copilot-instructions.md in the workspace (the file VS Code / GitHub Copilot actually reads) and emitted this additionalContext. This injector only emits additionalContext, which is a Claude Code session mechanism that Copilot does not consume — so for a Copilot-instructions plugin, the instructions no longer reach Copilot. If the delivery model intentionally pivoted to Claude-Code-session-only injection, please call that out in the PR/Jira; otherwise the file-write half needs to be restored.

Comment on lines +160 to +167

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking — two regressions here.

  1. Wrong output key. This emits top-level { "additional_context": ... }, which is the Cursor hook format. This plugin's hooks.json is a Claude Code SessionStart hook (${CLAUDE_PLUGIN_ROOT}), which consumes hookSpecificOutput.additionalContext (camelCase, nested) — the shape that was here last round. As written, Claude Code will ignore the payload. Restore:
process.stdout.write(JSON.stringify({
  hookSpecificOutput: {
    hookEventName: "SessionStart",
    additionalContext: template,
  },
}));
  1. Missing .github/copilot-instructions.md write. The Cursor copy doesn't materialize the workspace file. Copilot reads .github/copilot-instructions.md, not the Claude-Code additionalContext, so the write-when-absent block from last round needs to come back here (before this stdout emit). Without it, the instructions never reach Copilot — the plugin's primary purpose.

Loading