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

account.test.ts5.7 KB
// Ported from the Rust plugin's account.rs tests.
import * as Effect from "effect/Effect";
import assert from "node:assert/strict";
import { required, test, run as runEffect } from "./testing.ts";
import type { Api } from "convergence";
import {
  accountSummary,
  maintenance as maintenanceEffect,
  managerOf,
  parseAccount,
  recoveryIdentity,
  signedOutMessage,
} from "./account.ts";
import { fakeApi } from "../sdk/testing.ts";
import { clearCache } from "../sdk/maintenance.ts";

test("auth status becomes an account", () => {
  const account = parseAccount(
    '{"loggedIn": true, "authMethod": "claude.ai", "apiProvider": "firstParty", "email": "a@b.com", "subscriptionType": "max", "configDirectory": "/h/.claude"}',
  );
  assert.equal(required(account).loggedIn, true);
  assert.equal(required(account).email, "a@b.com");
  assert.equal(accountSummary(required(account)), "Signed in as a@b.com on the Max plan.");
  const out = parseAccount('{"loggedIn": false}');
  assert.equal(required(out).loggedIn, false);
  assert.equal(accountSummary(required(out)), null);
  assert.equal(parseAccount("not json"), null);
  assert.equal(
    signedOutMessage("/h/.claude_work"),
    "Run `claude auth login` with CLAUDE_CONFIG_DIR=/h/.claude_work to sign in.",
  );
  assert.equal(signedOutMessage(null), "Run `claude auth login` to sign in.");
});

/// A false claim of ownership makes Convergence run a package manager
/// against an install it did not create, so every branch needs proof.
test("only a proven path names an installer", () => {
  assert.deepEqual(managerOf("/Users/a/.local/bin/claude"), { manager: "native" });
  assert.deepEqual(managerOf("/Users/a/.local/share/claude/versions/2.1.271"), { manager: "native" });
  assert.deepEqual(managerOf("/opt/homebrew/lib/node_modules/@anthropic-ai/claude-code/cli.js"), {
    manager: "npm",
    prefix: "/opt/homebrew",
  });
  // A checkout is not a global install.
  assert.deepEqual(managerOf("/w/app/node_modules/x/lib/node_modules/@anthropic-ai/claude-code/cli.js"), {
    manager: "",
  });
  assert.deepEqual(managerOf("/opt/homebrew/bin/claude"), { manager: "" });
  assert.deepEqual(managerOf("/usr/local/bin/claude"), { manager: "" });
});

/// An install whose owner cannot be proven reports its version and nothing
/// else.
test("maintenance only offers an update it can run", async () => {
  clearCache();
  const api = fakeApi({
    onHost: (method) =>
      method === "host/process.which" ? { path: "/usr/local/bin/claude", realPath: "/usr/local/bin/claude" } : {},
  });
  const { out } = await maintenance(api, "2.1.271 (Claude Code)");
  assert.deepEqual(out, { canUpdate: false, installedVersion: "2.1.271 (Claude Code)" });
  assert.equal(api.peers.length, 0, "no registry is asked for an install nobody owns");
});

test("a native install is compared with the npm registry", async () => {
  clearCache();
  const api = fakeApi({
    onHost: (method) =>
      method === "host/process.which"
        ? { path: "/Users/a/.local/bin/claude", realPath: "/Users/a/.local/share/claude/versions/2.1.271" }
        : {},
    onSpawn: (program, args) => {
      assert.equal(program, "npm");
      assert.deepEqual(args, ["view", "@anthropic-ai/claude-code", "version"]);
      return () => {};
    },
  });
  const pending = maintenance(api, "2.1.271 (Claude Code)");
  for (let i = 0; i < 20 && !api.peers.length; i += 1) await new Promise((resolve) => setTimeout(resolve, 1));
  api.peers[0].stdout.push("2.1.300\n");
  api.peers[0].exit(0);
  const { out } = await pending;
  assert.deepEqual(out, {
    canUpdate: true,
    installedVersion: "2.1.271 (Claude Code)",
    latestVersion: "2.1.300",
    manager: "native",
  });
});

const maintenance = (api: Api, version: string) => runEffect(Effect.scoped(maintenanceEffect(version)), api);

test("non-object auth output is not a signed-out account", () => {
  for (const output of ["null", "true", "42", '"signed out"']) assert.equal(parseAccount(output), null);
  assert.deepEqual(parseAccount("[]"), {
    loggedIn: false,
    email: null,
    plan: null,
    apiProvider: null,
    authMethod: null,
    configDirectory: null,
    orgId: null,
    orgName: null,
  });
});

test("subscription recovery identity requires matching runtime and auth/config evidence, not cached account labels", () => {
  const status = {
    loggedIn: true,
    authMethod: "claude.ai",
    email: "a@b.com",
    apiProvider: "firstParty",
    configDirectory: "/h/.claude",
    orgId: "org-1",
    orgName: "Work",
  };
  const native = { email: "a@b.com", organization: "Work", apiProvider: "firstParty" };
  const account = parseAccount(JSON.stringify(status));
  const identity = recoveryIdentity(native, account, "/h/.claude/");
  assert.deepEqual(JSON.parse(required(identity)), ["claude/oauth", "/h/.claude", "a@b.com", "org-1"]);
  for (const patch of [
    { loggedIn: false },
    { authMethod: "api_key" },
    { apiProvider: "bedrock" },
    { email: "other@b.com" },
    { orgId: undefined },
    { orgName: "Other org" },
    { configDirectory: undefined },
    { configDirectory: "/different" },
  ])
    assert.equal(recoveryIdentity(native, parseAccount(JSON.stringify({ ...status, ...patch })), "/h/.claude"), null);
  for (const patch of [
    { email: undefined },
    { organization: undefined },
    { apiProvider: undefined },
    { apiProvider: "gateway" },
    { tokenSource: "CLAUDE_CODE_OAUTH_TOKEN" },
    { apiKeySource: "ANTHROPIC_API_KEY" },
  ])
    assert.equal(recoveryIdentity({ ...native, ...patch }, account, "/h/.claude"), null);
  assert.equal(recoveryIdentity(native, null, "/h/.claude"), null);
  assert.equal(recoveryIdentity(native, account, null), null);
  assert.notEqual(
    recoveryIdentity(native, parseAccount(JSON.stringify({ ...status, orgId: "org-2" })), "/h/.claude"),
    identity,
  );
});

Versions

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

Reviews and comments

0 threads · 0 reviews

No comments yet.