Skip to content

MLD-1307 - Auto-run plugin mcp.json align - #50

Open
arielamitjfrog wants to merge 9 commits into
mainfrom
feature/MLD-1307-auto-run-plugin-mcp-json-align
Open

MLD-1307 - Auto-run plugin mcp.json align#50
arielamitjfrog wants to merge 9 commits into
mainfrom
feature/MLD-1307-auto-run-plugin-mcp-json-align

Conversation

@arielamitjfrog

@arielamitjfrog arielamitjfrog commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Summary

Wire Claude Code SessionStart / FileChanged hooks so installed-plugin .mcp.json files are auto-aligned through @jfrog/agent-guard --align-plugin-mcps.

Also expands the vendored AI Catalog skill for agent plugins (rename + references) and adds a light entitlement gate on plugin download that reuses the existing MCP Agent Guard check.

Align hooks

  • claude-align-plugin-mcps.mjs — Claude adapter: spawn npx @jfrog/agent-guard --align-plugin-mcps --format hook-session-start|hook-file-changed, passthrough stdout (watchPaths / systemMessage / additionalContext owned by agent-guard), never fail the session.
  • claude-register-align-watch-paths.mjs — fast SessionStart companion: emit FileChanged watchPaths for installed_plugins.json / known_marketplaces.json before the slower npx align finishes, so mid-session plugin installs are watched while Agent Guard is still downloading.
  • hooks/hooks.json — register both on SessionStart (exec form command + args); re-run align on FileChanged when those metadata files change.
  • Tests / CI — modules/claude-align-plugin-mcps.test.mjs + workflow / CONTRIBUTING coverage.
  • Docs — README documents SessionStart/FileChanged align, kill switch, and unpinned npx behavior.
  • Version — plugin bumped to 0.2.19.
    Discovery + rewrite stay in agent-guard (--align-plugin-mcps). This PR only owns Claude hook UX, env forwarding, soft-fail, early watchPath registration, and related docs/CI.

AI Catalog skill

  • Rename jfrog-ai-catalog-skillsjfrog-ai-catalog (skills + plugins).
  • Add plugin reference docs: discover / install / manage / publish.
  • Plugin install/update entitlement: before the first download, run the shared MCP gate (jfrog-mcp-management/scripts/jfrog-agent-guard-check.mjs) and interpret exit codes via agent-guard-activation.md. No duplicated check script in this skill (simplified after an earlier draft). Listing installed plugins and remove remain local-only / ungated.

Goal

  • Auto-run plugin MCP align on every Claude session start.
  • Re-align when Claude plugin install metadata changes mid-session.
  • Keep the session usable even if npx / agent-guard fails (exit 0 always; SessionStart still registers watchPaths).
  • Share kill switch + registry / project / config-dir forwarding with agent-guard.
  • Gate plugin downloads on Agent Guard / AI Catalog entitlement without forking the MCP check implementation.

Assumptions

This adapter does not rewrite mcp.json itself and does not invent Claude hook payload shapes. Agent Guard owns discovery, transform, and hook stdout. The Claude plugin must:

  • Invoke npx --yes --registry <...> @jfrog/agent-guard --align-plugin-mcps --format hook-...
  • Forward optional JF_PROJECT--project, CLAUDE_CONFIG_DIR--claude-config-dir, JFROG_AGENT_GUARD_REPO → npx registry + agent-guard --registry
  • Soft-fail: never exit non-zero; on SessionStart failure / empty stdout, still emit fallback watchPaths
  • Honor JF_AGENT_ALIGN_PLUGIN_MCPS_DISABLE=1 (no-op, no stdout)
  • Remote MCP entries remain agent-guard's concern (never rewritten there)

Happy flow

SessionStart:

  1. claude-register-align-watch-paths.mjs prints Claude SessionStart JSON with watchPaths for ~/.claude/plugins/installed_plugins.json and known_marketplaces.json (or $CLAUDE_CONFIG_DIR/plugins/...).
  2. claude-align-plugin-mcps.mjs session-start runs agent-guard with --format hook-session-start.
  3. Agent-guard discovers installed-plugin .mcp.json paths, rewrites stdio MCPs to launch via Agent Guard, and prints Claude hook stdout (watchPaths + optional reload guidance).
  4. Adapter passthroughs that stdout unchanged.

FileChanged (matcher: installed_plugins.json|known_marketplaces.json):

  1. claude-align-plugin-mcps.mjs file-changed runs with --format hook-file-changed.
  2. Same rewrite core; stdout passthrough.

Idempotency / drift (owned by agent-guard; adapter just re-invokes):

  1. Re-run with the same args → rewritten: 0 (idempotent).
  2. Change project/registry/server → rewrite again (drift).

Kill switch: JF_AGENT_ALIGN_PLUGIN_MCPS_DISABLE=1 → both hooks exit 0 with no stdout / no spawn.

Plugin install (skill): before jf agent plugins install / update, run the shared Agent Guard activation check; proceed only on exit 0.

Non-happy flows

Expected behavior / exit (adapter always exit 0):

  • Kill switch set → no spawn, no stdout (exit 0).
  • Unknown mode arg → no-op, no spawn (exit 0).
  • agent-guard / npx fails on SessionStart → log error; emit fallback watchPaths so FileChanged still works (exit 0).
  • agent-guard exits 0 but empty SessionStart stdout → emit fallback watchPaths (exit 0).
  • agent-guard / npx fails on FileChanged → log error; write nothing (exit 0).
  • Missing JF_PROJECT → still invoke agent-guard (no --project); agent-guard decides whether project is required for rewrite (exit 0).
  • Private registry via JFROG_AGENT_GUARD_REPO → used for both npx --registry and agent-guard --registry.
  • Align timeout (~40s, under 45s hook budget) → SIGTERM child (process group on POSIX); SessionStart still emits fallback watchPaths (exit 0).

@arielamitjfrog
arielamitjfrog requested a review from a team as a code owner August 6, 2026 11:46
@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

All contributors have signed the CLA ✍️ ✅
Posted by the CLA Assistant Lite bot.

@arielamitjfrog

Copy link
Copy Markdown
Contributor Author

I have read the CLA Document and I hereby sign the CLA

@@ -0,0 +1,356 @@
#!/usr/bin/env node

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.

This file (plus claude-align-plugin-mcps.test.mjs, claude-register-align-watch-paths.mjs, and the skills/jfrog-ai-catalog/* additions) is hand-committed into directories that VENDOR.md documents as wholesale-vendored: modules/ from jfrog-agent-hooks, skills/jfrog-ai-catalog-skills/ from jfrog/jfrog-skills. Both directories get wholesale-replaced by their sync scripts on the next pin bump (sync-modules-vendor.json / sync-skills-vendor.json weren't touched here). This exact mistake was already made and reverted once, with the lesson written down in commit 8aa1671 ("Drop hand-committed MCP skills; they arrive via vendored sync ... committing them here is redundant and would be overwritten on the next pin bump"). Recent history shows both syncs happen routinely (module syncs fc235ef/d899b22/5d6ba15, skill syncs f2088f7/8aa1671/320a558/427d2e6) — the next one will silently delete this PR's work unless it lands upstream (in jfrog-agent-hooks / jfrog-skills) first.

const HARNESS_ID = "claude_code";
const log = createLogger("align-plugin-mcps");

export const AGENT_GUARD_PACKAGE = "@jfrog/agent-guard";

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.

@jfrog/agent-guard is installed via unpinned npx --yes, from a registry fully controllable via JFROG_AGENT_GUARD_REPO, on every single SessionStart. This is trust-on-first-use repeated every run rather than once — whoever controls that package tag/registry gets code execution in the Claude Code process on every session, on every machine with this plugin installed. Worth a version pin (or shipping this disabled by default) rather than relying on operators to discover the README's mirroring advice.

stdio: /** @type {const} */ (["pipe", "pipe", "pipe"]),
env,
// Resolve npx.cmd via cmd.exe; bare spawn("npx") often ENOENTs on Windows.
shell: isWin,

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.

On Windows, spawn(command, args, {shell:true}) is used with args built from env-derived values (JF_PROJECT, CLAUDE_CONFIG_DIR, JFROG_AGENT_GUARD_REPO) with no escaping/quoting layer. Node's spawn+shell:true on Windows joins array args into a single command-line string without per-argument quoting for cmd.exe, so a value containing a space — e.g. a completely normal Windows path like C:\Users\John Smith\.claude — gets corrupted/split rather than passed through intact. This will affect ordinary Windows users with spaces in their profile path, not just a theoretical attacker-controlled input.

Comment thread modules/claude-align-plugin-mcps.mjs Outdated
}

try {
child?.kill?.("SIGTERM");

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.

On Windows, killAlignChildTree falls through to child.kill('SIGTERM') on the single spawned process. Because shell:true makes cmd.exe the immediate child on Windows (unlike the POSIX branch's own process group), killing cmd.exe does not cascade-terminate the npx.cmd/node.exe grandchildren it launched — Windows doesn't auto-terminate child processes the way POSIX process groups do. A timeout-triggered kill on Windows can leave the real hung npx/node process running as an orphan indefinitely.


if (timeoutMs > 0) {
timer = setTimeout(() => {
killAlignChildTree(child, {

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.

killAlignChildTree only ever sends SIGTERM — no escalation to SIGKILL if the child doesn't die promptly, and no grace-period wait before finish() resolves and the parent process exits. This is exactly the failure mode the timeout exists to prevent: a genuinely stuck npx (e.g. a stalled network download) may not respond to SIGTERM at all, and can survive as an orphan after the Claude Code hook has already exited.

Comment thread modules/claude-align-plugin-mcps.mjs Outdated
const env = deps.env ?? process.env;
const writeStdout = deps.writeStdout ?? ((s) => process.stdout.write(s));
const readStdinFn = deps.readStdinFn ?? readStdin;
const format = MODES[modeArg];

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.

const format = MODES[modeArg] is a plain bracket lookup on a frozen plain object with no Object.hasOwn guard. If modeArg ever named an inherited Object.prototype member (e.g. "toString", "constructor"), MODES[modeArg] would return that inherited value (truthy), silently bypassing the if (!format) unknown-mode guard below. Unreachable today since modeArg only ever comes from the two hardcoded hooks.json literals, but worth tightening (Object.hasOwn(MODES, modeArg) ? MODES[modeArg] : undefined, or a Map) so it stays correct if this function is ever invoked another way.

});

try {
child.stdin?.end(stdin);

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.

For file-changed mode, the raw Claude hook stdin — read via the shared readStdin(), which settles after a ~50ms idle gap with no new data — is forwarded verbatim as agent-guard's own subprocess stdin. If the payload is large enough or arrives with any inter-chunk gap at or above that idle window, readStdin() can resolve with a truncated string, which then gets handed to agent-guard as if it were the complete FileChanged payload. This forwarding is new in this PR; previously this value was only used locally (e.g. parseSessionId, which tolerates parse failures gracefully).

Comment thread modules/claude-align-plugin-mcps.mjs Outdated
import { pathToFileURL } from "node:url";

import { createLogger, setLogContext } from "./core/logger.mjs";
import { readStdin, parseSessionId } from "./core/io.mjs";

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.

This adapter (and claude-register-align-watch-paths.mjs) doesn't call detectHarness(), unlike the existing adapters (claude-session-start.mjs, cursor-session-start.mjs), which use it specifically to avoid double-firing when Cursor and Claude Code read overlapping hook config. This particular hooks.json lives in this plugin's own directory rather than a shared global config file, so the practical double-fire risk is lower than for the adapters that do guard — but it's an inconsistency with the established defensive pattern in this codebase worth a conscious call rather than an omission.

Comment thread hooks/hooks.json Outdated
],
"FileChanged": [
{
"matcher": "installed_plugins.json|known_marketplaces.json",

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.

This FileChanged matcher contains literal . characters. Per Claude Code's hooks documentation, any matcher character outside [A-Za-z0-9_|] forces evaluation as an unanchored JavaScript regex — so . means "match any single character" here, not a literal dot, and the whole pattern is an unanchored substring match. This means it would also match unintended filenames like installed_pluginsXjson (any char for X) or installed_plugins.json.bak, not just the two intended exact filenames. Escaping the dots (installed_plugins\.json|known_marketplaces\.json) would fix this.

Comment thread hooks/hooks.json Outdated
"${CLAUDE_PLUGIN_ROOT}/modules/claude-align-plugin-mcps.mjs",
"session-start"
],
"timeout": 45,

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.

This SessionStart hook's timeout is 45s, and the sibling FileChanged hook is the same. The only prior timeout history in this file is a deliberate reduction (10s → 7s, commit 6d443ba) aimed at keeping session start snappy. 45s is over 6x that previous ceiling, and three SessionStart hooks now stack up (7s + 5s + 45s worst-case) on every session start. The 45s figure is justified by this being a network-bound operation with its own internal 40s kill-switch, so it's not necessarily wrong — but it's a large enough shift from this file's established direction that it's worth a deliberate, discussed sign-off rather than landing as part of a larger PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants