Skip to content

Commit 86efb61

Browse files
committed
trust only same-scope skill dependencies
1 parent 866b2f6 commit 86efb61

3 files changed

Lines changed: 19 additions & 8 deletions

File tree

packages/ui/src/features/loops/hooks/useLoopSkillBundles.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ async function buildSkillUploads(
4242
throw new Error(
4343
`The ${skill.name} skill references /${untrusted.name}, which resolved to a skill ` +
4444
`outside its own scope (${untrusted.source}: ${untrusted.path}). Install ` +
45-
`${untrusted.name} as one of your user skills, or alongside ${skill.name}, and retry.`,
45+
`${untrusted.name} alongside ${skill.name} and retry.`,
4646
);
4747
}
4848
const ordered = [skill, ...dependencies];

packages/ui/src/features/loops/loopSkill.test.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,20 @@ describe("isTrustedSkillDependency", () => {
2626

2727
it.each([
2828
{
29-
name: "user dependency is always trusted",
29+
name: "user dependency of a repo skill is rejected",
30+
// A repository's SKILL.md controls dependency names; trusting user skills
31+
// here would let it exfiltrate the user's private machine-level skills.
3032
dep: { source: "user" as const, path: "/home/me/.claude/skills/helper" },
3133
primary: repoPrimary,
34+
expected: false,
35+
},
36+
{
37+
name: "user dependency of a user skill is trusted",
38+
dep: { source: "user" as const, path: "/home/me/.claude/skills/helper" },
39+
primary: {
40+
source: "user" as const,
41+
path: "/home/me/.claude/skills/deploy-checks",
42+
},
3243
expected: true,
3344
},
3445
{

packages/ui/src/features/loops/loopSkill.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -55,17 +55,17 @@ function parentDir(skillPath: string): string {
5555

5656
/**
5757
* Whether a resolved skill dependency may ride along in a loop's bundle set.
58-
* The resolver matches dependencies by name across every local skill, so a
59-
* same-named skill in another opened repository could otherwise shadow the
60-
* real dependency and get itself installed into every recurring run. Trusted:
61-
* the user's own machine-level skills, and siblings from the same skills
62-
* directory as the skill that referenced them.
58+
* Dependency names come from the referencing skill's SKILL.md, which for a repo
59+
* skill is repository-controlled — so only same-source siblings from the same
60+
* skills directory are trusted. Anything looser leaks: a cross-directory match
61+
* lets another repo shadow the dependency, and trusting user skills from a repo
62+
* primary would let a repository declare likely user-skill names and silently
63+
* upload the user's private machine-level skills.
6364
*/
6465
export function isTrustedSkillDependency(
6566
dep: { source: LoopSchemas.LoopSkillSourceEnum; path: string },
6667
primary: { source: LoopSchemas.LoopSkillSourceEnum; path: string },
6768
): boolean {
68-
if (dep.source === "user") return true;
6969
return (
7070
dep.source === primary.source &&
7171
parentDir(dep.path) === parentDir(primary.path)

0 commit comments

Comments
 (0)