Official

claude

Claude Code agent provider: runs the Claude Code CLI headless for each account.

The app opens the listing; nothing installs until an agent in your Plugins workspace has read the files and you enable the plugin. In a terminal: cvg install convergence/claude@0.2.0

Permissions in 0.2.0

  • Provide agents agents.provideMediumAdds agents to the app.Provide the Claude Code agent and pass its tool calls to plugin tools
  • Run named programs processMediumStarts the listed programs.Run the Claude Code CLI (sessions, sign-in, `claude update`), and ask the npm installation that owns it about a newer versionPrograms: claudenpm
  • Read files fs.readMediumReads files in the listed places.Read Claude Code's sessions, settings and skills (in ~/.claude, other accounts' ~/.claude-* folders, the shared skills and the folder you choose in Settings), the project's .claude folder, the administrator's skill policy, and once, what the previous provider keptPlaces: ~/.claude/**~/.claude*/**~/.agents/skills/**the open workspaceits own data folder/Library/Application Support/ClaudeCode/managed-settings.json/etc/claude-code/managed-settings.json${settings.configDir}
  • Environment variables envMediumReads the listed environment variables.Find Claude Code's configuration directory, and the launch settings you set in CLAUDE_* variables (extra arguments, MCP servers, setting sources, appended system prompt, extra folders)Variables: HOMECLAUDE_CONFIG_DIRCLAUDE_EXTRA_ARGSCLAUDE_MCP_CONFIGCLAUDE_STRICT_MCP_CONFIGCLAUDE_SETTING_SOURCESCLAUDE_APPEND_SYSTEM_PROMPTCLAUDE_ADD_DIRS

Files

skills.test.ts4.6 KB
// Ported from the Rust plugin's skills.rs tests.
import assert from "node:assert/strict";
import { useApi, test, run as runEffect, files, tempDir } from "./testing.ts";
import { mkdirSync, writeFileSync, symlinkSync } from "node:fs";
import { join } from "node:path";
import { frontmatter, listSkills } from "./skills.ts";
import { apiFiles } from "./files.ts";
import { diskFs, fakeApi } from "../sdk/testing.ts";

function writeSkill(root: string, name: string, body: string) {
  mkdirSync(join(root, name), { recursive: true });
  writeFileSync(join(root, name, "SKILL.md"), body);
}

test("frontmatter reads the three keys that matter", () => {
  const fields = frontmatter(
    "---\nname: review\ndescription: Reviews code\ndisable-model-invocation: true\n---\nbody\n",
  );
  assert.equal(fields.description, "Reviews code");
  assert.equal(fields.disableModelInvocation, true);
  assert.equal(fields.userInvocable, true);
  // A file with no frontmatter is still a skill, without a description.
  assert.deepEqual(frontmatter("just a body\n"), {
    description: "",
    disableModelInvocation: false,
    userInvocable: true,
  });
  assert.equal(frontmatter("---\nuser-invocable: false\n---\n").userInvocable, false);
  // The body is not read as frontmatter once the block closed.
  assert.equal(frontmatter("---\ndescription: a\n---\ndescription: b\n").description, "a");
  assert.equal(frontmatter('---\ndescription: "quoted"\n---\n').description, "quoted");
});

test("the config directory's copy wins a name clash", async () => {
  const home = tempDir();
  const workspace = tempDir();
  writeSkill(join(home, "skills"), "review", "---\ndescription: user copy\n---\n");
  writeSkill(join(workspace, ".claude/skills"), "review", "---\ndescription: project copy\n---\n");
  writeSkill(join(workspace, ".claude/skills"), "ship", "---\ndescription: ship it\n---\n");
  const skills = await runEffect(listSkills(files(), workspace, home));
  assert.deepEqual(skills, [
    { name: "review", description: "user copy", source: "user", manualOnly: false },
    { name: "ship", description: "ship it", source: "project", manualOnly: false },
  ]);
});

test("settings switch skills off and make them manual", async () => {
  const home = tempDir();
  const workspace = tempDir();
  writeFileSync(
    join(home, "settings.json"),
    JSON.stringify({ skillOverrides: { hidden: "off", manual: "user-invocable-only", plain: "on" } }),
  );
  for (const name of ["hidden", "manual", "plain"])
    writeSkill(join(home, "skills"), name, "---\ndescription: d\n---\n");
  let skills = await runEffect(listSkills(files(), workspace, home));
  assert.deepEqual(
    skills.map((skill) => skill.name),
    ["manual", "plain"],
  );
  assert.equal(skills[0].manualOnly, true);
  assert.equal(skills[1].manualOnly, false);
  // One unreadable value drops the whole map rather than applying half of
  // a policy the administrator wrote.
  writeFileSync(join(home, "settings.json"), JSON.stringify({ skillOverrides: { hidden: "off", manual: true } }));
  skills = await runEffect(listSkills(files(), workspace, home));
  assert.equal(skills.length, 3);
});

test("disable-model-invocation becomes manual only", async () => {
  const home = tempDir();
  const workspace = tempDir();
  writeSkill(join(home, "skills"), "audit", "---\ndescription: d\ndisable-model-invocation: true\n---\n");
  writeSkill(join(home, "skills"), "modelonly", "---\ndescription: d\nuser-invocable: false\n---\n");
  const skills = await runEffect(listSkills(files(), workspace, home));
  assert.equal(skills.length, 1, "a skill the user cannot start does not belong in the picker");
  assert.equal(skills[0].manualOnly, true);
});

/// The broker checks a read against the path it resolves to, so a skill
/// linked in from outside the grants cannot be read; the CLI's command
/// list still names it.
test("a skill whose file no grant covers comes from the CLI's commands", async () => {
  const home = tempDir();
  const elsewhere = tempDir();
  const workspace = tempDir();
  writeSkill(elsewhere, "linked", "---\ndescription: from the file\n---\n");
  mkdirSync(join(home, "skills"), { recursive: true });
  symlinkSync(join(elsewhere, "linked"), join(home, "skills", "linked"));
  const granted = (useApi(fakeApi({ fs: diskFs({ allow: (path) => !path.includes("/linked/") }) })), apiFiles());
  const skills = await runEffect(
    listSkills(granted, workspace, home, [{ name: "linked", description: "from the CLI" }]),
  );
  assert.deepEqual(skills, [{ name: "linked", description: "from the CLI", source: "user", manualOnly: false }]);
  assert.deepEqual(await runEffect(listSkills(granted, workspace, home, [])), [], "nothing proves it exists");
});

Versions

VersionPublishedPlugin APISizePermissionsStatus
0.2.0latestOct 5, 2026>=2 <3128.7 KB4 permissionsListed

Reviews and comments

0 threads · 0 reviews

No comments yet.