fix(skills): ignore Python venvs and caches in skills watcher (#12399)

* fix(skills): ignore Python venvs and caches in skills watcher

Add .venv, venv, __pycache__, .mypy_cache, .pytest_cache, build, and
.cache to the default ignored patterns for the skills watcher.

This prevents file descriptor exhaustion when a skill contains a Python
virtual environment with tens of thousands of files, which was causing
EBADF spawn errors on macOS.

Fixes #1056

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* docs: add changelog entry for skills watcher ignores

* docs: fill changelog PR number

---------

Co-authored-by: Kyle Howells <freerunnering@gmail.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: CLAWDINATOR Bot <clawdinator[bot]@users.noreply.github.com>
This commit is contained in:
clawdinator[bot]
2026-02-09 06:41:53 +00:00
committed by GitHub
parent 8d96955e19
commit 6ed255319f
3 changed files with 36 additions and 1 deletions

View File

@@ -105,6 +105,8 @@ Docs: https://docs.openclaw.ai
- Heartbeat: allow explicit accountId routing for multi-account channels. (#8702) Thanks @lsh411.
- Routing: refresh bindings per message by loading config at route resolution so binding changes apply without restart. (#11372) Thanks @juanpablodlc.
- TUI/Gateway: handle non-streaming finals, refresh history for non-local chat runs, and avoid event gap warnings for targeted tool streams. (#8432) Thanks @gumadeiras.
- Security: stop exposing Gateway auth tokens via URL query parameters in Control UI entrypoints, and reject hook tokens in query parameters. (#9436) Thanks @coygeek.
- Skills: ignore Python venvs and common cache/build folders in the skills watcher to prevent FD exhaustion. (#12399) Thanks @kylehowells.
- Shell completion: auto-detect and migrate slow dynamic patterns to cached files for faster terminal startup; add completion health checks to doctor/update/onboard.
- Telegram: honor session model overrides in inline model selection. (#8193) Thanks @gildo.
- Web UI: fix agent model selection saves for default/non-default agents and wrap long workspace paths. Thanks @Takhoffman.

View File

@@ -12,7 +12,7 @@ vi.mock("chokidar", () => {
});
describe("ensureSkillsWatcher", () => {
it("ignores node_modules, dist, and .git by default", async () => {
it("ignores node_modules, dist, .git, and Python venvs by default", async () => {
const mod = await import("./refresh.js");
mod.ensureSkillsWatcher({ workspaceDir: "/tmp/workspace" });
@@ -21,11 +21,35 @@ describe("ensureSkillsWatcher", () => {
expect(opts.ignored).toBe(mod.DEFAULT_SKILLS_WATCH_IGNORED);
const ignored = mod.DEFAULT_SKILLS_WATCH_IGNORED;
// Node/JS paths
expect(ignored.some((re) => re.test("/tmp/workspace/skills/node_modules/pkg/index.js"))).toBe(
true,
);
expect(ignored.some((re) => re.test("/tmp/workspace/skills/dist/index.js"))).toBe(true);
expect(ignored.some((re) => re.test("/tmp/workspace/skills/.git/config"))).toBe(true);
// Python virtual environments and caches
expect(ignored.some((re) => re.test("/tmp/workspace/skills/scripts/.venv/bin/python"))).toBe(
true,
);
expect(ignored.some((re) => re.test("/tmp/workspace/skills/venv/lib/python3.10/site.py"))).toBe(
true,
);
expect(ignored.some((re) => re.test("/tmp/workspace/skills/__pycache__/module.pyc"))).toBe(
true,
);
expect(ignored.some((re) => re.test("/tmp/workspace/skills/.mypy_cache/3.10/foo.json"))).toBe(
true,
);
expect(ignored.some((re) => re.test("/tmp/workspace/skills/.pytest_cache/v/cache"))).toBe(true);
// Build artifacts and caches
expect(ignored.some((re) => re.test("/tmp/workspace/skills/build/output.js"))).toBe(true);
expect(ignored.some((re) => re.test("/tmp/workspace/skills/.cache/data.json"))).toBe(true);
// Should NOT ignore normal skill files
expect(ignored.some((re) => re.test("/tmp/.hidden/skills/index.md"))).toBe(false);
expect(ignored.some((re) => re.test("/tmp/workspace/skills/my-skill/SKILL.md"))).toBe(false);
});
});

View File

@@ -29,6 +29,15 @@ export const DEFAULT_SKILLS_WATCH_IGNORED: RegExp[] = [
/(^|[\\/])\.git([\\/]|$)/,
/(^|[\\/])node_modules([\\/]|$)/,
/(^|[\\/])dist([\\/]|$)/,
// Python virtual environments and caches
/(^|[\\/])\.venv([\\/]|$)/,
/(^|[\\/])venv([\\/]|$)/,
/(^|[\\/])__pycache__([\\/]|$)/,
/(^|[\\/])\.mypy_cache([\\/]|$)/,
/(^|[\\/])\.pytest_cache([\\/]|$)/,
// Build artifacts and caches
/(^|[\\/])build([\\/]|$)/,
/(^|[\\/])\.cache([\\/]|$)/,
];
function bumpVersion(current: number): number {