From c783f720c77d5ce2fb1ad21716c30de78d2824a8 Mon Sep 17 00:00:00 2001 From: Thomas Kosiewski Date: Thu, 22 Oct 2026 09:39:20 +0000 Subject: [PATCH 1/2] =?UTF-8?q?=F0=9F=A4=96=20fix:=20start=20the=20shown?= =?UTF-8?q?=20skills=20CLI=20command=20in=20the=20home=20folder?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit npx prefers a matching skills package in the working folder's node_modules over the registry, so the copied command run inside an attacker's checkout would start the planted copy. The command now changes to the home folder first (cmd.exe: cd /d "%USERPROFILE%"). Refs #7123 (item 35). --- docs/agents/agent-skills.mdx | 2 +- .../Sections/PluginInstallMethods.tsx | 5 ++- .../PluginsSettingsSection.stories.tsx | 2 +- src/browser/stories/mocks/orpc.ts | 2 +- .../services/agentPlugins/skillsCli.test.ts | 44 ++++++++++++++++++- src/node/services/agentPlugins/skillsCli.ts | 23 ++++++++-- .../builtInSkillContent.generated.ts | 2 +- 7 files changed, 70 insertions(+), 10 deletions(-) diff --git a/docs/agents/agent-skills.mdx b/docs/agents/agent-skills.mdx index fce8fe3adaf..fabb42ff25f 100644 --- a/docs/agents/agent-skills.mdx +++ b/docs/agents/agent-skills.mdx @@ -96,7 +96,7 @@ Xum reads one MCP file: an `mcpServers` path replaces `.mcp.json` instead of add #### Skills repositories -**Settings → Plugins** also installs a repository of plain skills (`SKILL.md` folders, without a plugin manifest) as a managed plugin. Xum finds the skills with the rules of the [skills CLI](https://github.com/vercel-labs/skills) 1.7.4: a valid `SKILL.md` at the root, or skill folders directly below the root and up to three levels below `skills/`, `.agents/skills/`, `.claude/skills/`, `.github/skills/` and the other folders that the CLI searches. Xum writes a `plugin.json` for them (named after the repository or the plugin folder), and the install gets the same review, component selection and updates as any plugin. A skills install loads skills only: no hooks, agents, workflows or MCP servers. Xum tries the install methods in this order: Agent Plugin (`plugin.json` or `.codex-plugin/plugin.json`), Claude Code plugin, then Skills. When more than one method fits, the preview shows an **Install as** choice. Skills never replace a repository's own `plugin.json` or `.codex-plugin/plugin.json`. The **Skills CLI** choice shows the command of the pinned skills CLI for the checked skills, for you to copy and run in a terminal. Xum does not run the CLI itself: the CLI clones the repository again and follows symlinks when it copies a skill, so its run can copy files that the preview did not show. The command uses `--global --agent codex`, which copies the skills into `~/.agents/skills` (with `--agent mux`, as above, the CLI writes `~/.mux/skills` instead), and the CLI asks before it installs. Xum does not review, update or remove skills that the CLI installs. The choice is offered only for a source made of letters, digits and `@ _ + = : , . / -`, so the command is safe to paste into any shell. +**Settings → Plugins** also installs a repository of plain skills (`SKILL.md` folders, without a plugin manifest) as a managed plugin. Xum finds the skills with the rules of the [skills CLI](https://github.com/vercel-labs/skills) 1.7.4: a valid `SKILL.md` at the root, or skill folders directly below the root and up to three levels below `skills/`, `.agents/skills/`, `.claude/skills/`, `.github/skills/` and the other folders that the CLI searches. Xum writes a `plugin.json` for them (named after the repository or the plugin folder), and the install gets the same review, component selection and updates as any plugin. A skills install loads skills only: no hooks, agents, workflows or MCP servers. Xum tries the install methods in this order: Agent Plugin (`plugin.json` or `.codex-plugin/plugin.json`), Claude Code plugin, then Skills. When more than one method fits, the preview shows an **Install as** choice. Skills never replace a repository's own `plugin.json` or `.codex-plugin/plugin.json`. The **Skills CLI** choice shows the command of the pinned skills CLI for the checked skills, for you to copy and run in a terminal. Xum does not run the CLI itself: the CLI clones the repository again and follows symlinks when it copies a skill, so its run can copy files that the preview did not show. The command uses `--global --agent codex`, which copies the skills into `~/.agents/skills` (with `--agent mux`, as above, the CLI writes `~/.mux/skills` instead), and the CLI asks before it installs. Xum does not review, update or remove skills that the CLI installs. The choice is offered only for a source made of letters, digits and `@ _ + = : , . / -`, so the command is safe to paste into any shell. The command first changes to your home folder (`cd ~` or `cd /d "%USERPROFILE%"` on Windows), because npx would otherwise prefer a `skills` package from the current folder's `node_modules`. #### Component review diff --git a/src/browser/features/Settings/Sections/PluginInstallMethods.tsx b/src/browser/features/Settings/Sections/PluginInstallMethods.tsx index 2c831eba403..f2b2540d8b9 100644 --- a/src/browser/features/Settings/Sections/PluginInstallMethods.tsx +++ b/src/browser/features/Settings/Sections/PluginInstallMethods.tsx @@ -258,7 +258,10 @@ export const SkillsCliInstall: React.FC<{ /> {cli.command !== null && (
-

Run this command in a terminal:

+

+ Run this command in a terminal. It starts in your home folder, so npx cannot pick up a + skills package from a project's node_modules. +

{cli.command} diff --git a/src/browser/features/Settings/Sections/PluginsSettingsSection.stories.tsx b/src/browser/features/Settings/Sections/PluginsSettingsSection.stories.tsx index 2680d488532..d26e0acb224 100644 --- a/src/browser/features/Settings/Sections/PluginsSettingsSection.stories.tsx +++ b/src/browser/features/Settings/Sections/PluginsSettingsSection.stories.tsx @@ -1318,7 +1318,7 @@ export const AddPluginSkillsCliCommand: Story = { await userEvent.click(canvas.getByRole("checkbox", { name: "frontend-design" })); await userEvent.click(canvas.getByRole("checkbox", { name: "skill-creator" })); await userEvent.click(canvas.getByRole("button", { name: "Show command (1)" })); - await canvas.findByText("Run this command in a terminal:"); + await canvas.findByText(/Run this command in a terminal\. It starts in your home folder/); await canvas.findByText(/--skill pdf --global --agent codex --copy$/); await canvas.findByRole("button", { name: /copy/i }); }, diff --git a/src/browser/stories/mocks/orpc.ts b/src/browser/stories/mocks/orpc.ts index 62ca757ae2a..e9f22dc509d 100644 --- a/src/browser/stories/mocks/orpc.ts +++ b/src/browser/stories/mocks/orpc.ts @@ -1444,7 +1444,7 @@ export function createMockORPCClient(options: MockORPCClientOptions = {}): APICl success: true as const, data: { command: [ - "npx --yes skills@1.7.4 add", + "cd ~ && npx --yes skills@1.7.4 add", input.source, ...input.skills.map((skill) => `--skill ${skill}`), "--global --agent codex --copy", diff --git a/src/node/services/agentPlugins/skillsCli.test.ts b/src/node/services/agentPlugins/skillsCli.test.ts index 459662af1f8..74b5ff2d958 100644 --- a/src/node/services/agentPlugins/skillsCli.test.ts +++ b/src/node/services/agentPlugins/skillsCli.test.ts @@ -1,6 +1,18 @@ import { describe, expect, test } from "bun:test"; +import { execFile } from "node:child_process"; +import * as fs from "node:fs/promises"; +import * as os from "node:os"; +import * as path from "node:path"; +import { promisify } from "node:util"; -import { SKILLS_CLI_PACKAGE, skillsCliArgs, skillsCliCommandResult } from "./skillsCli"; +import { + SKILLS_CLI_PACKAGE, + skillsCliArgs, + skillsCliCommand, + skillsCliCommandResult, +} from "./skillsCli"; + +const execFileAsync = promisify(execFile); const REPO = "https://github.com/anthropics/skills.git"; @@ -62,7 +74,7 @@ test("the command holds only validated arguments; invalid input is the error bra ).toEqual({ success: true, data: { - command: `npx --yes ${SKILLS_CLI_PACKAGE} add git@github.com:anthropics/skills.git --skill pdf --global --agent codex --copy`, + command: `${process.platform === "win32" ? 'cd /d "%USERPROFILE%" &&' : "cd ~ &&"} npx --yes ${SKILLS_CLI_PACKAGE} add git@github.com:anthropics/skills.git --skill pdf --global --agent codex --copy`, }, }); for (const input of [ @@ -73,3 +85,31 @@ test("the command holds only validated arguments; invalid input is the error bra expect(skillsCliCommandResult(input).success).toBe(false); } }); + +// npx prefers a matching package in the working folder's node_modules over the registry, so the +// shown command must leave the folder the user runs it from before npx starts. +test.skipIf(process.platform === "win32")( + "the shown command starts npx in the home folder, not in the current folder", + async () => { + const root = await fs.mkdtemp(path.join(os.tmpdir(), "skills-cli-cwd-")); + try { + const home = path.join(root, "home"); + const checkout = path.join(root, "checkout"); + const bin = path.join(root, "bin"); + await fs.mkdir(home); + await fs.mkdir(path.join(checkout, "node_modules", "skills"), { recursive: true }); + await fs.mkdir(bin); + // A stand-in npx that reports the folder it starts in. + await fs.writeFile(path.join(bin, "npx"), "#!/bin/sh\npwd -P\n", { mode: 0o755 }); + const command = skillsCliCommand({ source: REPO, skills: ["pdf"] }, "linux"); + const { stdout } = await execFileAsync("/bin/sh", ["-c", command], { + cwd: checkout, + env: { PATH: `${bin}:/usr/bin:/bin`, HOME: home }, + encoding: "utf8", + }); + expect(stdout.trim()).toBe(await fs.realpath(home)); + } finally { + await fs.rm(root, { recursive: true, force: true }); + } + } +); diff --git a/src/node/services/agentPlugins/skillsCli.ts b/src/node/services/agentPlugins/skillsCli.ts index e5cfe5b4ba9..7c81d3e5c4a 100644 --- a/src/node/services/agentPlugins/skillsCli.ts +++ b/src/node/services/agentPlugins/skillsCli.ts @@ -91,11 +91,27 @@ function formatCommand(argv: string[]): string { return argv.join(" "); } +/** + * SECURITY: `npx skills@` prefers a matching package in the working folder's + * node_modules over the registry, so the copied command run inside an attacker's checkout would + * start the attacker's copy. The command therefore changes to the home folder first; `&&` stops + * it when that fails. A fixed constant per shell, never built from input. + */ +const SKILLS_CLI_CWD_PREFIX = { + posix: "cd ~ &&", + // cmd.exe has no `~`; `/d` also switches the drive. + win32: 'cd /d "%USERPROFILE%" &&', +} as const; + /** * The command for `input`: validated source and skill names only. Throws a user-facing error for - * invalid input. + * invalid input. `platform` picks the shell syntax of the folder change (the user's terminal runs + * on the same machine as the Xum backend). */ -export function skillsCliCommand(input: { source: string; skills: string[] }): string { +export function skillsCliCommand( + input: { source: string; skills: string[] }, + platform: NodeJS.Platform = process.platform +): string { const args = skillsCliArgs(input.source, input.skills); // skillsCliSourceProblem and the skill-name grammar admit only safe characters. Check what Xum // shows again: an unsafe argument throws here, it is never quoted. @@ -103,7 +119,8 @@ export function skillsCliCommand(input: { source: string; skills: string[] }): s args.every((arg) => SKILLS_CLI_SAFE_ARG_PATTERN.test(arg)), "skillsCliCommand: every argument must be safe to show unquoted in any shell" ); - return formatCommand(["npx", ...args]); + const prefix = platform === "win32" ? SKILLS_CLI_CWD_PREFIX.win32 : SKILLS_CLI_CWD_PREFIX.posix; + return `${prefix} ${formatCommand(["npx", ...args])}`; } /** The ORPC shape: invalid input becomes the error branch. */ diff --git a/src/node/services/agentSkills/builtInSkillContent.generated.ts b/src/node/services/agentSkills/builtInSkillContent.generated.ts index 0fe06337676..4cbc15d1fca 100644 --- a/src/node/services/agentSkills/builtInSkillContent.generated.ts +++ b/src/node/services/agentSkills/builtInSkillContent.generated.ts @@ -2420,7 +2420,7 @@ export const BUILTIN_SKILL_FILES: Record> = { "", "#### Skills repositories", "", - "**Settings → Plugins** also installs a repository of plain skills (`SKILL.md` folders, without a plugin manifest) as a managed plugin. Xum finds the skills with the rules of the [skills CLI](https://github.com/vercel-labs/skills) 1.7.4: a valid `SKILL.md` at the root, or skill folders directly below the root and up to three levels below `skills/`, `.agents/skills/`, `.claude/skills/`, `.github/skills/` and the other folders that the CLI searches. Xum writes a `plugin.json` for them (named after the repository or the plugin folder), and the install gets the same review, component selection and updates as any plugin. A skills install loads skills only: no hooks, agents, workflows or MCP servers. Xum tries the install methods in this order: Agent Plugin (`plugin.json` or `.codex-plugin/plugin.json`), Claude Code plugin, then Skills. When more than one method fits, the preview shows an **Install as** choice. Skills never replace a repository's own `plugin.json` or `.codex-plugin/plugin.json`. The **Skills CLI** choice shows the command of the pinned skills CLI for the checked skills, for you to copy and run in a terminal. Xum does not run the CLI itself: the CLI clones the repository again and follows symlinks when it copies a skill, so its run can copy files that the preview did not show. The command uses `--global --agent codex`, which copies the skills into `~/.agents/skills` (with `--agent mux`, as above, the CLI writes `~/.mux/skills` instead), and the CLI asks before it installs. Xum does not review, update or remove skills that the CLI installs. The choice is offered only for a source made of letters, digits and `@ _ + = : , . / -`, so the command is safe to paste into any shell.", + "**Settings → Plugins** also installs a repository of plain skills (`SKILL.md` folders, without a plugin manifest) as a managed plugin. Xum finds the skills with the rules of the [skills CLI](https://github.com/vercel-labs/skills) 1.7.4: a valid `SKILL.md` at the root, or skill folders directly below the root and up to three levels below `skills/`, `.agents/skills/`, `.claude/skills/`, `.github/skills/` and the other folders that the CLI searches. Xum writes a `plugin.json` for them (named after the repository or the plugin folder), and the install gets the same review, component selection and updates as any plugin. A skills install loads skills only: no hooks, agents, workflows or MCP servers. Xum tries the install methods in this order: Agent Plugin (`plugin.json` or `.codex-plugin/plugin.json`), Claude Code plugin, then Skills. When more than one method fits, the preview shows an **Install as** choice. Skills never replace a repository's own `plugin.json` or `.codex-plugin/plugin.json`. The **Skills CLI** choice shows the command of the pinned skills CLI for the checked skills, for you to copy and run in a terminal. Xum does not run the CLI itself: the CLI clones the repository again and follows symlinks when it copies a skill, so its run can copy files that the preview did not show. The command uses `--global --agent codex`, which copies the skills into `~/.agents/skills` (with `--agent mux`, as above, the CLI writes `~/.mux/skills` instead), and the CLI asks before it installs. Xum does not review, update or remove skills that the CLI installs. The choice is offered only for a source made of letters, digits and `@ _ + = : , . / -`, so the command is safe to paste into any shell. The command first changes to your home folder (`cd ~` or `cd /d \"%USERPROFILE%\"` on Windows), because npx would otherwise prefer a `skills` package from the current folder's `node_modules`.", "", "#### Component review", "", From b1a1e8cee737f1f2dfbafd4477c0241f3141986b Mon Sep 17 00:00:00 2001 From: Thomas Kosiewski Date: Thu, 22 Oct 2026 16:05:10 +0000 Subject: [PATCH 2/2] =?UTF-8?q?=F0=9F=A4=96=20fix:=20no=20cmd.exe=20prefix?= =?UTF-8?q?=20on=20Windows;=20the=20user=20runs=20the=20command=20from=20h?= =?UTF-8?q?ome?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Xum's Windows terminal prefers Git Bash or PowerShell, where cd /d and %USERPROFILE% (and && in Windows PowerShell) do not work. No single prefix works in every Windows shell, so the Windows command has none and Settings asks the user to run it from the home folder. macOS and Linux keep cd ~ &&. --- docs/agents/agent-skills.mdx | 2 +- .../Sections/PluginInstallMethods.tsx | 4 ++-- .../PluginsSettingsSection.stories.tsx | 2 +- .../services/agentPlugins/skillsCli.test.ts | 11 ++++++++++- src/node/services/agentPlugins/skillsCli.ts | 19 ++++++++----------- .../builtInSkillContent.generated.ts | 2 +- 6 files changed, 23 insertions(+), 17 deletions(-) diff --git a/docs/agents/agent-skills.mdx b/docs/agents/agent-skills.mdx index fabb42ff25f..aa5520a3ff6 100644 --- a/docs/agents/agent-skills.mdx +++ b/docs/agents/agent-skills.mdx @@ -96,7 +96,7 @@ Xum reads one MCP file: an `mcpServers` path replaces `.mcp.json` instead of add #### Skills repositories -**Settings → Plugins** also installs a repository of plain skills (`SKILL.md` folders, without a plugin manifest) as a managed plugin. Xum finds the skills with the rules of the [skills CLI](https://github.com/vercel-labs/skills) 1.7.4: a valid `SKILL.md` at the root, or skill folders directly below the root and up to three levels below `skills/`, `.agents/skills/`, `.claude/skills/`, `.github/skills/` and the other folders that the CLI searches. Xum writes a `plugin.json` for them (named after the repository or the plugin folder), and the install gets the same review, component selection and updates as any plugin. A skills install loads skills only: no hooks, agents, workflows or MCP servers. Xum tries the install methods in this order: Agent Plugin (`plugin.json` or `.codex-plugin/plugin.json`), Claude Code plugin, then Skills. When more than one method fits, the preview shows an **Install as** choice. Skills never replace a repository's own `plugin.json` or `.codex-plugin/plugin.json`. The **Skills CLI** choice shows the command of the pinned skills CLI for the checked skills, for you to copy and run in a terminal. Xum does not run the CLI itself: the CLI clones the repository again and follows symlinks when it copies a skill, so its run can copy files that the preview did not show. The command uses `--global --agent codex`, which copies the skills into `~/.agents/skills` (with `--agent mux`, as above, the CLI writes `~/.mux/skills` instead), and the CLI asks before it installs. Xum does not review, update or remove skills that the CLI installs. The choice is offered only for a source made of letters, digits and `@ _ + = : , . / -`, so the command is safe to paste into any shell. The command first changes to your home folder (`cd ~` or `cd /d "%USERPROFILE%"` on Windows), because npx would otherwise prefer a `skills` package from the current folder's `node_modules`. +**Settings → Plugins** also installs a repository of plain skills (`SKILL.md` folders, without a plugin manifest) as a managed plugin. Xum finds the skills with the rules of the [skills CLI](https://github.com/vercel-labs/skills) 1.7.4: a valid `SKILL.md` at the root, or skill folders directly below the root and up to three levels below `skills/`, `.agents/skills/`, `.claude/skills/`, `.github/skills/` and the other folders that the CLI searches. Xum writes a `plugin.json` for them (named after the repository or the plugin folder), and the install gets the same review, component selection and updates as any plugin. A skills install loads skills only: no hooks, agents, workflows or MCP servers. Xum tries the install methods in this order: Agent Plugin (`plugin.json` or `.codex-plugin/plugin.json`), Claude Code plugin, then Skills. When more than one method fits, the preview shows an **Install as** choice. Skills never replace a repository's own `plugin.json` or `.codex-plugin/plugin.json`. The **Skills CLI** choice shows the command of the pinned skills CLI for the checked skills, for you to copy and run in a terminal. Xum does not run the CLI itself: the CLI clones the repository again and follows symlinks when it copies a skill, so its run can copy files that the preview did not show. The command uses `--global --agent codex`, which copies the skills into `~/.agents/skills` (with `--agent mux`, as above, the CLI writes `~/.mux/skills` instead), and the CLI asks before it installs. Xum does not review, update or remove skills that the CLI installs. The choice is offered only for a source made of letters, digits and `@ _ + = : , . / -`, so the command is safe to paste into any shell. Run it from your home folder: npx prefers a `skills` package from the current folder's `node_modules`. On macOS and Linux the command starts with `cd ~ &&` for this. On Windows it has no such prefix, because no single form works in cmd.exe, PowerShell and Git Bash. #### Component review diff --git a/src/browser/features/Settings/Sections/PluginInstallMethods.tsx b/src/browser/features/Settings/Sections/PluginInstallMethods.tsx index f2b2540d8b9..9362e617317 100644 --- a/src/browser/features/Settings/Sections/PluginInstallMethods.tsx +++ b/src/browser/features/Settings/Sections/PluginInstallMethods.tsx @@ -259,8 +259,8 @@ export const SkillsCliInstall: React.FC<{ {cli.command !== null && (

- Run this command in a terminal. It starts in your home folder, so npx cannot pick up a - skills package from a project's node_modules. + Run this command in a terminal, from your home folder. Inside a project, npx would + prefer a skills package from that project's node_modules.

diff --git a/src/browser/features/Settings/Sections/PluginsSettingsSection.stories.tsx b/src/browser/features/Settings/Sections/PluginsSettingsSection.stories.tsx index d26e0acb224..5bb1f081a1b 100644 --- a/src/browser/features/Settings/Sections/PluginsSettingsSection.stories.tsx +++ b/src/browser/features/Settings/Sections/PluginsSettingsSection.stories.tsx @@ -1318,7 +1318,7 @@ export const AddPluginSkillsCliCommand: Story = { await userEvent.click(canvas.getByRole("checkbox", { name: "frontend-design" })); await userEvent.click(canvas.getByRole("checkbox", { name: "skill-creator" })); await userEvent.click(canvas.getByRole("button", { name: "Show command (1)" })); - await canvas.findByText(/Run this command in a terminal\. It starts in your home folder/); + await canvas.findByText(/Run this command in a terminal, from your home folder/); await canvas.findByText(/--skill pdf --global --agent codex --copy$/); await canvas.findByRole("button", { name: /copy/i }); }, diff --git a/src/node/services/agentPlugins/skillsCli.test.ts b/src/node/services/agentPlugins/skillsCli.test.ts index 74b5ff2d958..90538a080af 100644 --- a/src/node/services/agentPlugins/skillsCli.test.ts +++ b/src/node/services/agentPlugins/skillsCli.test.ts @@ -74,7 +74,7 @@ test("the command holds only validated arguments; invalid input is the error bra ).toEqual({ success: true, data: { - command: `${process.platform === "win32" ? 'cd /d "%USERPROFILE%" &&' : "cd ~ &&"} npx --yes ${SKILLS_CLI_PACKAGE} add git@github.com:anthropics/skills.git --skill pdf --global --agent codex --copy`, + command: `${process.platform === "win32" ? "" : "cd ~ && "}npx --yes ${SKILLS_CLI_PACKAGE} add git@github.com:anthropics/skills.git --skill pdf --global --agent codex --copy`, }, }); for (const input of [ @@ -86,6 +86,15 @@ test("the command holds only validated arguments; invalid input is the error bra } }); +// Windows terminals differ (cmd.exe, PowerShell 5 and 7, Git Bash), and no folder-change prefix +// works in all of them, so the Windows command has none: Settings asks the user to run it from +// the home folder instead. +test("the Windows command has no shell-specific prefix", () => { + const command = skillsCliCommand({ source: REPO, skills: ["pdf"] }, "win32"); + expect(command.startsWith("npx --yes ")).toBe(true); + expect(skillsCliCommand({ source: REPO, skills: ["pdf"] }, "darwin")).toBe(`cd ~ && ${command}`); +}); + // npx prefers a matching package in the working folder's node_modules over the registry, so the // shown command must leave the folder the user runs it from before npx starts. test.skipIf(process.platform === "win32")( diff --git a/src/node/services/agentPlugins/skillsCli.ts b/src/node/services/agentPlugins/skillsCli.ts index 7c81d3e5c4a..28140527c9f 100644 --- a/src/node/services/agentPlugins/skillsCli.ts +++ b/src/node/services/agentPlugins/skillsCli.ts @@ -94,19 +94,16 @@ function formatCommand(argv: string[]): string { /** * SECURITY: `npx skills@` prefers a matching package in the working folder's * node_modules over the registry, so the copied command run inside an attacker's checkout would - * start the attacker's copy. The command therefore changes to the home folder first; `&&` stops - * it when that fails. A fixed constant per shell, never built from input. + * start the attacker's copy. On macOS and Linux the command changes to the home folder first + * (`&&` stops it when that fails); `cd ~ &&` works in bash, zsh and fish. Windows has no prefix + * that works in cmd.exe, Windows PowerShell 5, PowerShell 7 and Git Bash alike, so there the + * command has none and Settings tells the user to run it from the home folder. */ -const SKILLS_CLI_CWD_PREFIX = { - posix: "cd ~ &&", - // cmd.exe has no `~`; `/d` also switches the drive. - win32: 'cd /d "%USERPROFILE%" &&', -} as const; +const SKILLS_CLI_POSIX_CWD_PREFIX = "cd ~ &&"; /** * The command for `input`: validated source and skill names only. Throws a user-facing error for - * invalid input. `platform` picks the shell syntax of the folder change (the user's terminal runs - * on the same machine as the Xum backend). + * invalid input. `platform` is the user's machine (the terminal runs where the Xum backend runs). */ export function skillsCliCommand( input: { source: string; skills: string[] }, @@ -119,8 +116,8 @@ export function skillsCliCommand( args.every((arg) => SKILLS_CLI_SAFE_ARG_PATTERN.test(arg)), "skillsCliCommand: every argument must be safe to show unquoted in any shell" ); - const prefix = platform === "win32" ? SKILLS_CLI_CWD_PREFIX.win32 : SKILLS_CLI_CWD_PREFIX.posix; - return `${prefix} ${formatCommand(["npx", ...args])}`; + const command = formatCommand(["npx", ...args]); + return platform === "win32" ? command : `${SKILLS_CLI_POSIX_CWD_PREFIX} ${command}`; } /** The ORPC shape: invalid input becomes the error branch. */ diff --git a/src/node/services/agentSkills/builtInSkillContent.generated.ts b/src/node/services/agentSkills/builtInSkillContent.generated.ts index 4cbc15d1fca..1c18b597d5c 100644 --- a/src/node/services/agentSkills/builtInSkillContent.generated.ts +++ b/src/node/services/agentSkills/builtInSkillContent.generated.ts @@ -2420,7 +2420,7 @@ export const BUILTIN_SKILL_FILES: Record> = { "", "#### Skills repositories", "", - "**Settings → Plugins** also installs a repository of plain skills (`SKILL.md` folders, without a plugin manifest) as a managed plugin. Xum finds the skills with the rules of the [skills CLI](https://github.com/vercel-labs/skills) 1.7.4: a valid `SKILL.md` at the root, or skill folders directly below the root and up to three levels below `skills/`, `.agents/skills/`, `.claude/skills/`, `.github/skills/` and the other folders that the CLI searches. Xum writes a `plugin.json` for them (named after the repository or the plugin folder), and the install gets the same review, component selection and updates as any plugin. A skills install loads skills only: no hooks, agents, workflows or MCP servers. Xum tries the install methods in this order: Agent Plugin (`plugin.json` or `.codex-plugin/plugin.json`), Claude Code plugin, then Skills. When more than one method fits, the preview shows an **Install as** choice. Skills never replace a repository's own `plugin.json` or `.codex-plugin/plugin.json`. The **Skills CLI** choice shows the command of the pinned skills CLI for the checked skills, for you to copy and run in a terminal. Xum does not run the CLI itself: the CLI clones the repository again and follows symlinks when it copies a skill, so its run can copy files that the preview did not show. The command uses `--global --agent codex`, which copies the skills into `~/.agents/skills` (with `--agent mux`, as above, the CLI writes `~/.mux/skills` instead), and the CLI asks before it installs. Xum does not review, update or remove skills that the CLI installs. The choice is offered only for a source made of letters, digits and `@ _ + = : , . / -`, so the command is safe to paste into any shell. The command first changes to your home folder (`cd ~` or `cd /d \"%USERPROFILE%\"` on Windows), because npx would otherwise prefer a `skills` package from the current folder's `node_modules`.", + "**Settings → Plugins** also installs a repository of plain skills (`SKILL.md` folders, without a plugin manifest) as a managed plugin. Xum finds the skills with the rules of the [skills CLI](https://github.com/vercel-labs/skills) 1.7.4: a valid `SKILL.md` at the root, or skill folders directly below the root and up to three levels below `skills/`, `.agents/skills/`, `.claude/skills/`, `.github/skills/` and the other folders that the CLI searches. Xum writes a `plugin.json` for them (named after the repository or the plugin folder), and the install gets the same review, component selection and updates as any plugin. A skills install loads skills only: no hooks, agents, workflows or MCP servers. Xum tries the install methods in this order: Agent Plugin (`plugin.json` or `.codex-plugin/plugin.json`), Claude Code plugin, then Skills. When more than one method fits, the preview shows an **Install as** choice. Skills never replace a repository's own `plugin.json` or `.codex-plugin/plugin.json`. The **Skills CLI** choice shows the command of the pinned skills CLI for the checked skills, for you to copy and run in a terminal. Xum does not run the CLI itself: the CLI clones the repository again and follows symlinks when it copies a skill, so its run can copy files that the preview did not show. The command uses `--global --agent codex`, which copies the skills into `~/.agents/skills` (with `--agent mux`, as above, the CLI writes `~/.mux/skills` instead), and the CLI asks before it installs. Xum does not review, update or remove skills that the CLI installs. The choice is offered only for a source made of letters, digits and `@ _ + = : , . / -`, so the command is safe to paste into any shell. Run it from your home folder: npx prefers a `skills` package from the current folder's `node_modules`. On macOS and Linux the command starts with `cd ~ &&` for this. On Windows it has no such prefix, because no single form works in cmd.exe, PowerShell and Git Bash.", "", "#### Component review", "",