diff --git a/CHANGELOG.md b/CHANGELOG.md index 2b40e90b035..8b899482513 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -117,6 +117,7 @@ Docs: https://docs.openclaw.ai - CLI/Commander: preserve Commander-computed exit codes for argument and help-error paths, and cover the user-argv parse mode in the regression tests so invalid CLI invocations no longer report success when exits are intercepted. (#60923) Thanks @Linux2010. - Telegram/native command menu: trim long menu descriptions before dropping commands so sub-100 command sets can still fit Telegram's payload budget and keep more `/` entries visible. (#61129) Thanks @neeravmakwana. - Agents/Claude CLI: keep non-interactive `--permission-mode bypassPermissions` when custom `cliBackends.claude-cli.args` override defaults, so cron and heartbeat Claude CLI runs do not regress to interactive approval mode. (#61114) Thanks @cathrynlavery and @thewilloftheshadow. +- Agents/skills: skip `.git` and `node_modules` when mirroring skills into sandbox workspaces so read-only sandboxes do not copy repo history or dependency trees. (#61090) Thanks @joelnishanth. ## 2026.4.2 diff --git a/src/agents/skills.build-workspace-skills-prompt.syncs-merged-skills-into-target-workspace.test.ts b/src/agents/skills.build-workspace-skills-prompt.syncs-merged-skills-into-target-workspace.test.ts index c74559bdb20..5b9f87a9241 100644 --- a/src/agents/skills.build-workspace-skills-prompt.syncs-merged-skills-into-target-workspace.test.ts +++ b/src/agents/skills.build-workspace-skills-prompt.syncs-merged-skills-into-target-workspace.test.ts @@ -100,6 +100,15 @@ describe("buildWorkspaceSkillsPrompt", () => { const extraDir = path.join(sourceWorkspace, ".extra"); const bundledDir = path.join(sourceWorkspace, ".bundled"); const managedDir = path.join(sourceWorkspace, ".managed"); + const workspaceSkillDir = path.join(sourceWorkspace, "skills", "demo-skill"); + + await fs.mkdir(path.join(workspaceSkillDir, ".git"), { recursive: true }); + await fs.writeFile(path.join(workspaceSkillDir, ".git", "config"), "gitdir"); + await fs.mkdir(path.join(workspaceSkillDir, "node_modules", "pkg"), { recursive: true }); + await fs.writeFile( + path.join(workspaceSkillDir, "node_modules", "pkg", "index.js"), + "export {}", + ); await withEnv({ HOME: sourceWorkspace, PATH: "" }, () => syncSkillsToWorkspace({ @@ -121,6 +130,12 @@ describe("buildWorkspaceSkillsPrompt", () => { expect(prompt).not.toContain("Bundled version"); expect(prompt).not.toContain("Extra version"); expect(prompt.replaceAll("\\", "/")).toContain("demo-skill/SKILL.md"); + expect(await pathExists(path.join(targetWorkspace, "skills", "demo-skill", ".git"))).toBe( + false, + ); + expect( + await pathExists(path.join(targetWorkspace, "skills", "demo-skill", "node_modules")), + ).toBe(false); }); it("syncs the explicit agent skill subset instead of inherited defaults", async () => { diff --git a/src/agents/skills/workspace.ts b/src/agents/skills/workspace.ts index 57ea27ba466..2b51a6d80b1 100644 --- a/src/agents/skills/workspace.ts +++ b/src/agents/skills/workspace.ts @@ -825,6 +825,10 @@ export async function syncSkillsToWorkspace(params: { await fsp.cp(entry.skill.baseDir, dest, { recursive: true, force: true, + filter: (src) => { + const name = path.basename(src); + return !(name === ".git" || name === "node_modules"); + }, }); } catch (error) { const message = error instanceof Error ? error.message : JSON.stringify(error);