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
Files
skills.ts6 KB
// Skill discovery (the Rust `skills.rs`).
//
// The CLI publishes skills as slash commands, which says nothing about
// where a skill came from or whether the model may start it on its own.
// So the plugin reads the same two folders the CLI reads, in the same
// precedence order, and applies the same `skillOverrides` setting.
//
// The broker checks a read against the path it resolves to, so a skill
// folder that is a link out of the granted folders (the `skills`
// installer links `~/.claude/skills/<name>` into `~/.agents/skills`)
// cannot be read. Such a skill is still offered when the CLI lists it as
// a command, with the command's description.
import * as wire from "./wire.ts";
import * as Effect from "effect/Effect";
import type { Files } from "./files.ts";
import type { Skill } from "./types.ts";
import { join, readJson } from "./files.ts";
/// Where a skill lives, in precedence order: a name found in an earlier
/// root wins.
const ROOTS: [Skill["source"], string][] = [
["user", "skills"],
["project", ".claude/skills"],
];
/// The administrator's policy file, which wins over everything the user
/// wrote.
export const MANAGED_SETTINGS = [
"/Library/Application Support/ClaudeCode/managed-settings.json",
"/etc/claude-code/managed-settings.json",
];
function parseOverride(value: unknown) {
if (value === "off") return "off";
if (value === "user-invocable-only") return "user-invocable-only";
if (value === "on" || value === "name-only") return "on";
return null;
}
/// The settings files the CLI merges, lowest precedence first. The
/// repository root above a workspace is outside every grant and is left
/// out.
function settingsFiles(workspace: string, configDir: string | null) {
const out = [];
if (configDir) out.push(join(configDir, "settings.json"));
out.push(
join(workspace, ".claude/settings.json"),
join(workspace, ".claude/settings.local.json"),
...MANAGED_SETTINGS,
);
return out;
}
/// The merged `skillOverrides` map. One unknown value drops every
/// override in that file, as the CLI does: a half-read policy file would
/// enable a skill the administrator meant to switch off.
export const overrides = Effect.fn("Claude.overrides")(function* (
files: Files,
workspace: string,
configDir: string | null,
) {
const merged = new Map<string, "off" | "on" | "user-invocable-only">();
for (const file of settingsFiles(workspace, configDir)) {
const settings = yield* readJson(files, file);
const entries = wire.objectOf(settings).skillOverrides;
if (!entries || typeof entries !== "object" || Array.isArray(entries)) continue;
const parsed = Object.entries(entries).map(
([name, value]) => [name, typeof value === "string" ? parseOverride(value) : null] as const,
);
if (parsed.some(([, value]) => value === null)) {
console.debug(`claude: ignoring skillOverrides with an unknown value in ${file}`);
continue;
}
for (const [name, value] of parsed) if (value !== null) merged.set(name, value);
}
return merged;
});
/// Reads the `---` block at the top of a `SKILL.md`. Only flat `key:
/// value` pairs are read: the three keys that matter are scalars.
export function frontmatter(body: string) {
const fields = { description: "", disableModelInvocation: false, userInvocable: true };
if (!body.startsWith("---")) return fields;
let rest = body.slice(3);
if (rest.startsWith("\r\n")) rest = rest.slice(2);
else if (rest.startsWith("\n")) rest = rest.slice(1);
else return fields;
for (const raw of rest.split("\n")) {
const line = raw.replace(/\s+$/, "");
if (line === "---") break;
const colon = line.indexOf(":");
if (colon < 0) continue;
const key = line.slice(0, colon).trim();
const value = line
.slice(colon + 1)
.trim()
.replace(/^["']+|["']+$/g, "");
if (key === "description") fields.description = value;
else if (key === "disable-model-invocation")
fields.disableModelInvocation = ["true", "yes", "on", "y", "1"].includes(value.toLowerCase());
else if (key === "user-invocable")
fields.userInvocable = !["false", "no", "off", "n", "0"].includes(value.toLowerCase());
}
return fields;
}
/// Every skill the CLI would offer in `workspace`, the user's copy winning
/// a name clash with the project's. `commands` are the CLI's slash
/// commands, the fallback for a skill whose file cannot be read.
export const listSkills = Effect.fn("Claude.listSkills")(function* (
files: Files,
workspace: string,
configDir: string | null,
commands: wire.Command[] = [],
) {
const policy = yield* overrides(files, workspace, configDir);
const found = new Map<string, Skill>();
for (const [source, relative] of ROOTS) {
if (source === "user" && !configDir) continue;
const root = source === "user" ? join(configDir, relative) : join(workspace, relative);
const entries = yield* files.list(root);
if (!entries) continue;
const names = entries
.map((entry) => entry.name.trim())
.filter((name) => name)
.sort();
for (const name of names) {
if (found.has(name)) continue;
const over = policy.get(name) ?? null;
if (over === "off") continue;
const body = yield* files.text(join(root, name, "SKILL.md"));
if (body === null) {
const command = commands.find((command) => command?.name === name);
if (command) {
found.set(name, {
name,
description: command.description ?? "",
source,
manualOnly: over === "user-invocable-only",
});
}
continue;
}
const fields = frontmatter(body);
// A skill the user cannot start has no place in a picker the user
// drives; the model still reaches it through its own tool.
if (!fields.userInvocable) continue;
found.set(name, {
name,
description: fields.description,
source,
manualOnly: fields.disableModelInvocation || over === "user-invocable-only",
});
}
}
return [...found.keys()].sort().map((name) => found.get(name)!); // Names are the map's own keys.
});Versions
| Version | Published | Plugin API | Size | Permissions | Status |
|---|---|---|---|---|---|
| 0.2.0latest | Oct 5, 2026 | >=2 <3 | 128.7 KB | 4 permissions | Listed |
No comments yet.