Skip to content

Commit cf881a7

Browse files
TMHSDigitalclaude
andcommitted
feat(distribution): plugin ships the rules as a skill; skill file refs work outside a checkout
After #289 the plugin installed the skills, but the rules needed a second, hand-maintained clone imported from CLAUDE.md, and 7 skills pointed at repo-root paths (`snippets/...`, `examples/...`) that Claude resolves against the user's project, where they do not exist. The rules summary promised links that were plain text. - scripts/build_claude_rules.py now also writes claude/skills/blender-rules/SKILL.md (name + trigger description; same body as claude/blender-rules.md), and every "Full rule:" is an absolute GitHub link. --check covers both outputs. - .claude-plugin/plugin.json "skills": ["./claude/skills/"]; per the plugin manifest reference this adds to the default skills/ scan. Kept outside skills/ so the 16-skill inventory and Cursor manifest are unchanged. - 38 bare repo paths in 7 SKILL.md files became absolute GitHub links (blob for files, tree for directories), each verified to exist. - tests/check_skill_refs.py (in validate-claude-packaging): no bare repo path in skills or the rules summaries, and every repo link resolves. Falsified: the pre-change ai-mesh-cleanup skill reports its bare `examples/...` paths. - validate-claude-packaging checks the plugin.json skills entries exist and validates SKILL.md frontmatter in every declared skills dir. - README Quick start / Cursor section and the landing install step: the rules arrive with the plugin as the blender-rules skill; the CLAUDE.md import is now the optional always-on path. Closes #334 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent d59b154 commit cf881a7

15 files changed

Lines changed: 233 additions & 66 deletions

File tree

‎.claude-plugin/plugin.json‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@
99
"homepage": "https://github.com/TMHSDigital/Blender-Developer-Tools",
1010
"repository": "https://github.com/TMHSDigital/Blender-Developer-Tools",
1111
"license": "CC-BY-NC-ND-4.0 AND MIT",
12+
"skills": [
13+
"./claude/skills/"
14+
],
1215
"keywords": [
1316
"claude-code",
1417
"blender",

‎.github/workflows/validate.yml‎

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -245,8 +245,19 @@ jobs:
245245
elif not os.path.isdir(os.path.join(entries[0].get('source', ''), 'skills')):
246246
errors.append('marketplace.json source must point at a directory containing skills/')
247247
248-
# Claude Code discovers skills/<name>/SKILL.md; each needs name + description.
249-
for path in sorted(glob.glob('skills/*/SKILL.md')):
248+
# Claude Code discovers skills/<name>/SKILL.md plus every extra
249+
# directory in plugin.json "skills" (the generated blender-rules
250+
# skill); each SKILL.md needs name + description.
251+
extra = plugin.get('skills', [])
252+
skill_dirs = ['skills']
253+
for d in extra:
254+
if not (isinstance(d, str) and d.startswith('./') and os.path.isdir(d)):
255+
errors.append(f'plugin.json skills entry {d!r} must be an existing ./ directory')
256+
else:
257+
skill_dirs.append(d)
258+
if './claude/skills/' not in extra:
259+
errors.append('plugin.json skills must include ./claude/skills/ (the rules skill)')
260+
for path in sorted(p for d in skill_dirs for p in glob.glob(os.path.join(d, '*', 'SKILL.md'))):
250261
parts = open(path, encoding='utf-8').read().replace('\r\n', '\n').split('---')
251262
front = parts[1] if len(parts) > 2 else ''
252263
for key in ('name:', 'description:'):
@@ -277,9 +288,12 @@ jobs:
277288
exit 1
278289
fi
279290
280-
- name: Check claude/blender-rules.md is regenerated from rules/*.mdc
291+
- name: Check claude/blender-rules.md and the rules skill are regenerated from rules/*.mdc
281292
run: python3 scripts/build_claude_rules.py --check
282293

294+
- name: Check skill file references resolve outside a checkout
295+
run: python3 tests/check_skill_refs.py
296+
283297
validate-counts:
284298
name: Validate content counts
285299
runs-on: ubuntu-latest

‎README.md‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,14 +61,14 @@ git clone https://github.com/TMHSDigital/Blender-Developer-Tools.git
6161
```
6262

6363
- **Cursor** — open your project with this checkout available and copy (or symlink) `rules/*.mdc` into your project's `.cursor/rules/`. The rules apply automatically by glob scope; skills are referenced by name in chat.
64-
- **Claude Code** — install as a plugin, then run `/skills` to see all 16 skills:
64+
- **Claude Code** — install as a plugin, then run `/skills` to see all 16 skills plus `blender-rules`:
6565

6666
```text
6767
/plugin marketplace add TMHSDigital/Blender-Developer-Tools
6868
/plugin install blender-developer-tools@blender-developer-tools
6969
```
7070

71-
Claude Code does not read Cursor `.mdc` rules. For the rules, add one line to your project's `CLAUDE.md` that imports the generated summary from a checkout: `@/path/to/Blender-Developer-Tools/claude/blender-rules.md`. Without the plugin, copy `skills/*` into your project's `.claude/skills/` instead.
71+
Claude Code does not read Cursor `.mdc` rules, so the plugin ships them as the `blender-rules` skill (generated from `rules/`), which Claude loads when it writes or reviews bpy code. No second clone is needed. To keep the rules in context all the time instead, add `@/path/to/Blender-Developer-Tools/claude/blender-rules.md` to your project's `CLAUDE.md` from a checkout. Without the plugin, copy `skills/*` and `claude/skills/*` into your project's `.claude/skills/`.
7272
- **Get Blender** — download **5.2 LTS** (primary target) or **4.5 LTS** (supported fallback) from [blender.org/download/lts](https://www.blender.org/download/lts/); current stable lives at [blender.org/download](https://www.blender.org/download/). The `blender` command below is that binary — on macOS it is inside the app bundle at `/Applications/Blender.app/Contents/MacOS/Blender`.
7373
- **Run an example** — every example is a self-checking headless script (exit non-zero on failure, no GPU needed for the check):
7474

@@ -81,7 +81,7 @@ blender --background --python examples/bmesh-gear/bmesh_gear.py --
8181
Releases ship often; [CHANGELOG.md](CHANGELOG.md) lists what each one changed.
8282

8383
- **Claude Code plugin** — `/plugin marketplace update blender-developer-tools` refreshes the marketplace and its plugins (from a shell: `claude plugin update blender-developer-tools@blender-developer-tools`). Remove it with `/plugin uninstall blender-developer-tools`, then `/plugin marketplace remove blender-developer-tools`.
84-
- **Checkout** (Cursor rules, the Claude rules import, examples) — `git pull` in the clone. Copied `.mdc` files do not update themselves: symlink `rules/*.mdc` into `.cursor/rules/` instead of copying, or re-copy after each pull. To uninstall, delete the copied or linked rules and the `@.../blender-rules.md` line from your `CLAUDE.md`.
84+
- **Checkout** (Cursor rules, the optional always-on Claude rules import, examples) — `git pull` in the clone. Copied `.mdc` files do not update themselves: symlink `rules/*.mdc` into `.cursor/rules/` instead of copying, or re-copy after each pull. To uninstall, delete the copied or linked rules and the `@.../blender-rules.md` line from your `CLAUDE.md`.
8585

8686
## Supported Blender versions
8787

@@ -1486,7 +1486,7 @@ The `.mdc` files in `rules/` apply automatically when Cursor opens a Blender Pyt
14861486
- `no-unapplied-modifiers-on-export`: flags export of objects with live modifiers when the export does not request evaluated geometry
14871487
- `use-correct-axis-rna-per-exporter`: flags `export_scene.gltf` calls that pass FBX `axis_forward` / `axis_up`, and `export_scene.fbx` calls that pass glTF `export_yup`
14881488

1489-
Cursor: copy `rules/*.mdc` into `.cursor/rules/`. Claude Code: install the plugin (see [Quick start](#quick-start)) and import `claude/blender-rules.md` from your `CLAUDE.md`.
1489+
Cursor: copy `rules/*.mdc` into `.cursor/rules/`. Claude Code: install the plugin (see [Quick start](#quick-start)); it ships the rules as the `blender-rules` skill.
14901490

14911491
## Using the templates
14921492

‎claude/blender-rules.md‎

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,52 +9,52 @@ full version with Wrong/Right examples.
99

1010
Flag bmesh.new() calls without a paired bm.free() in a try/finally block. BMesh allocates C-side memory that Python's garbage collector cannot reclaim; missing free() leaks and eventually crashes Blender.
1111

12-
Applies to: `**/*.py`. Full rule: `rules/always-free-bmesh.mdc`.
12+
Applies to: `**/*.py`. Full rule: [`rules/always-free-bmesh.mdc`](https://github.com/TMHSDigital/Blender-Developer-Tools/blob/main/rules/always-free-bmesh.mdc).
1313

1414
## no-unapplied-modifiers-on-export
1515

1616
Flag an export call on objects that still carry unapplied modifiers when the export arguments do not request evaluated geometry. The engine then receives the authored cage, not the modifier result.
1717

18-
Applies to: `**/*.py`. Full rule: `rules/no-unapplied-modifiers-on-export.mdc`.
18+
Applies to: `**/*.py`. Full rule: [`rules/no-unapplied-modifiers-on-export.mdc`](https://github.com/TMHSDigital/Blender-Developer-Tools/blob/main/rules/no-unapplied-modifiers-on-export.mdc).
1919

2020
## prefer-data-over-ops-in-loops
2121

2222
Flag bpy.ops.* calls inside iteration over many objects, meshes, or frames. Each bpy.ops call triggers a full depsgraph evaluation and UI redraw; loops slow down by orders of magnitude. Use bpy.data.* and bmesh instead.
2323

24-
Applies to: `**/*.py`. Full rule: `rules/prefer-data-over-ops-in-loops.mdc`.
24+
Applies to: `**/*.py`. Full rule: [`rules/prefer-data-over-ops-in-loops.mdc`](https://github.com/TMHSDigital/Blender-Developer-Tools/blob/main/rules/prefer-data-over-ops-in-loops.mdc).
2525

2626
## prefer-temp-override-over-context-copy
2727

2828
Flag uses of `bpy.context.copy()` to override context for an operator call. Passing a context dict to an operator was deprecated in Blender 3.2 and removed in 4.0, so it already fails on the 4.5 LTS fallback. Use `bpy.context.temp_override(**overrides)` as a context manager instead.
2929

30-
Applies to: `**/*.py`. Full rule: `rules/prefer-temp-override-over-context-copy.mdc`.
30+
Applies to: `**/*.py`. Full rule: [`rules/prefer-temp-override-over-context-copy.mdc`](https://github.com/TMHSDigital/Blender-Developer-Tools/blob/main/rules/prefer-temp-override-over-context-copy.mdc).
3131

3232
## target-extensions-platform-format
3333

3434
Flag new Blender add-ons that ship only a legacy bl_info dict without a blender_manifest.toml. New add-ons must use the Extensions Platform format. bl_info may appear alongside as a fallback for backward compatibility, but the manifest is the source of truth.
3535

36-
Applies to: `**/__init__.py`, `**/blender_manifest.toml`. Full rule: `rules/target-extensions-platform-format.mdc`.
36+
Applies to: `**/__init__.py`, `**/blender_manifest.toml`. Full rule: [`rules/target-extensions-platform-format.mdc`](https://github.com/TMHSDigital/Blender-Developer-Tools/blob/main/rules/target-extensions-platform-format.mdc).
3737

3838
## type-annotate-props-and-defend-context
3939

4040
Flag two related anti-patterns. (1) bpy.props defined as class-level assignments instead of type annotations (deprecated since 2.8). (2) Code that touches bpy.context.active_object without guarding for None.
4141

42-
Applies to: `**/*.py`. Full rule: `rules/type-annotate-props-and-defend-context.mdc`.
42+
Applies to: `**/*.py`. Full rule: [`rules/type-annotate-props-and-defend-context.mdc`](https://github.com/TMHSDigital/Blender-Developer-Tools/blob/main/rules/type-annotate-props-and-defend-context.mdc).
4343

4444
## use-correct-axis-rna-per-exporter
4545

4646
Flag export_scene.gltf calls that pass FBX axis_forward or axis_up, and export_scene.fbx calls that pass glTF export_yup. The two exporters do not share axis RNA.
4747

48-
Applies to: `**/*.py`. Full rule: `rules/use-correct-axis-rna-per-exporter.mdc`.
48+
Applies to: `**/*.py`. Full rule: [`rules/use-correct-axis-rna-per-exporter.mdc`](https://github.com/TMHSDigital/Blender-Developer-Tools/blob/main/rules/use-correct-axis-rna-per-exporter.mdc).
4949

5050
## use-foreach-set-for-bulk-data
5151

5252
Flag Python loops that set vertex coordinates, normals, UVs, or other bulk per-element data one element at a time. For meshes of more than a few thousand elements, this is 100x to 1000x slower than `mesh.vertices.foreach_set("co", flat_array)`, which writes through to C-level storage in one pass.
5353

54-
Applies to: `**/*.py`. Full rule: `rules/use-foreach-set-for-bulk-data.mdc`.
54+
Applies to: `**/*.py`. Full rule: [`rules/use-foreach-set-for-bulk-data.mdc`](https://github.com/TMHSDigital/Blender-Developer-Tools/blob/main/rules/use-foreach-set-for-bulk-data.mdc).
5555

5656
## validate-imported-mesh-scale
5757

5858
Flag a glTF or FBX import followed by mesh operations with no transform_apply and no unit-scale check. Generated files often arrive with non-identity object scale or a non-meter scene scale; mesh edits then bake the wrong size.
5959

60-
Applies to: `**/*.py`. Full rule: `rules/validate-imported-mesh-scale.mdc`.
60+
Applies to: `**/*.py`. Full rule: [`rules/validate-imported-mesh-scale.mdc`](https://github.com/TMHSDigital/Blender-Developer-Tools/blob/main/rules/validate-imported-mesh-scale.mdc).
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
---
2+
name: blender-rules
3+
description: Blender Python anti-pattern rules (bmesh leaks, bpy.ops in loops, deprecated context-copy overrides, bulk mesh writes, prop annotations and context guards, Extensions manifest, import scale, export modifiers, glTF/FBX axis RNA). Use when writing, reviewing or fixing any bpy / Blender add-on or script code.
4+
---
5+
6+
# Blender Developer Tools rules (Claude Code)
7+
8+
<!-- Generated by scripts/build_claude_rules.py from rules/*.mdc. Do not edit. -->
9+
10+
Apply these when writing or reviewing Blender Python. Each rule links to the
11+
full version with Wrong/Right examples.
12+
13+
## always-free-bmesh
14+
15+
Flag bmesh.new() calls without a paired bm.free() in a try/finally block. BMesh allocates C-side memory that Python's garbage collector cannot reclaim; missing free() leaks and eventually crashes Blender.
16+
17+
Applies to: `**/*.py`. Full rule: [`rules/always-free-bmesh.mdc`](https://github.com/TMHSDigital/Blender-Developer-Tools/blob/main/rules/always-free-bmesh.mdc).
18+
19+
## no-unapplied-modifiers-on-export
20+
21+
Flag an export call on objects that still carry unapplied modifiers when the export arguments do not request evaluated geometry. The engine then receives the authored cage, not the modifier result.
22+
23+
Applies to: `**/*.py`. Full rule: [`rules/no-unapplied-modifiers-on-export.mdc`](https://github.com/TMHSDigital/Blender-Developer-Tools/blob/main/rules/no-unapplied-modifiers-on-export.mdc).
24+
25+
## prefer-data-over-ops-in-loops
26+
27+
Flag bpy.ops.* calls inside iteration over many objects, meshes, or frames. Each bpy.ops call triggers a full depsgraph evaluation and UI redraw; loops slow down by orders of magnitude. Use bpy.data.* and bmesh instead.
28+
29+
Applies to: `**/*.py`. Full rule: [`rules/prefer-data-over-ops-in-loops.mdc`](https://github.com/TMHSDigital/Blender-Developer-Tools/blob/main/rules/prefer-data-over-ops-in-loops.mdc).
30+
31+
## prefer-temp-override-over-context-copy
32+
33+
Flag uses of `bpy.context.copy()` to override context for an operator call. Passing a context dict to an operator was deprecated in Blender 3.2 and removed in 4.0, so it already fails on the 4.5 LTS fallback. Use `bpy.context.temp_override(**overrides)` as a context manager instead.
34+
35+
Applies to: `**/*.py`. Full rule: [`rules/prefer-temp-override-over-context-copy.mdc`](https://github.com/TMHSDigital/Blender-Developer-Tools/blob/main/rules/prefer-temp-override-over-context-copy.mdc).
36+
37+
## target-extensions-platform-format
38+
39+
Flag new Blender add-ons that ship only a legacy bl_info dict without a blender_manifest.toml. New add-ons must use the Extensions Platform format. bl_info may appear alongside as a fallback for backward compatibility, but the manifest is the source of truth.
40+
41+
Applies to: `**/__init__.py`, `**/blender_manifest.toml`. Full rule: [`rules/target-extensions-platform-format.mdc`](https://github.com/TMHSDigital/Blender-Developer-Tools/blob/main/rules/target-extensions-platform-format.mdc).
42+
43+
## type-annotate-props-and-defend-context
44+
45+
Flag two related anti-patterns. (1) bpy.props defined as class-level assignments instead of type annotations (deprecated since 2.8). (2) Code that touches bpy.context.active_object without guarding for None.
46+
47+
Applies to: `**/*.py`. Full rule: [`rules/type-annotate-props-and-defend-context.mdc`](https://github.com/TMHSDigital/Blender-Developer-Tools/blob/main/rules/type-annotate-props-and-defend-context.mdc).
48+
49+
## use-correct-axis-rna-per-exporter
50+
51+
Flag export_scene.gltf calls that pass FBX axis_forward or axis_up, and export_scene.fbx calls that pass glTF export_yup. The two exporters do not share axis RNA.
52+
53+
Applies to: `**/*.py`. Full rule: [`rules/use-correct-axis-rna-per-exporter.mdc`](https://github.com/TMHSDigital/Blender-Developer-Tools/blob/main/rules/use-correct-axis-rna-per-exporter.mdc).
54+
55+
## use-foreach-set-for-bulk-data
56+
57+
Flag Python loops that set vertex coordinates, normals, UVs, or other bulk per-element data one element at a time. For meshes of more than a few thousand elements, this is 100x to 1000x slower than `mesh.vertices.foreach_set("co", flat_array)`, which writes through to C-level storage in one pass.
58+
59+
Applies to: `**/*.py`. Full rule: [`rules/use-foreach-set-for-bulk-data.mdc`](https://github.com/TMHSDigital/Blender-Developer-Tools/blob/main/rules/use-foreach-set-for-bulk-data.mdc).
60+
61+
## validate-imported-mesh-scale
62+
63+
Flag a glTF or FBX import followed by mesh operations with no transform_apply and no unit-scale check. Generated files often arrive with non-identity object scale or a non-meter scene scale; mesh edits then bake the wrong size.
64+
65+
Applies to: `**/*.py`. Full rule: [`rules/validate-imported-mesh-scale.mdc`](https://github.com/TMHSDigital/Blender-Developer-Tools/blob/main/rules/validate-imported-mesh-scale.mdc).

‎scripts/build_claude_rules.py‎

Lines changed: 40 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,18 @@
11
#!/usr/bin/env python3
2-
"""Generate claude/blender-rules.md from rules/*.mdc.
2+
"""Generate the Claude Code copies of rules/*.mdc.
33
4-
Claude Code does not read Cursor .mdc rules. This writes a plain-markdown
5-
summary (one section per rule: description, load scope, link to the full rule)
6-
that a Claude Code user imports from their own CLAUDE.md with
7-
``@path/to/Blender-Developer-Tools/claude/blender-rules.md``.
4+
Claude Code does not read Cursor .mdc rules. This writes the same plain-markdown
5+
summary (one section per rule: description, load scope, link to the full rule
6+
with its Wrong/Right examples) to two places:
7+
8+
- claude/skills/blender-rules/SKILL.md: a skill the plugin ships
9+
(.claude-plugin/plugin.json "skills"), so plugin users get the rules with no
10+
second clone; Claude loads it when writing or reviewing bpy code.
11+
- claude/blender-rules.md: for always-on use, imported from a project's
12+
CLAUDE.md with ``@path/to/Blender-Developer-Tools/claude/blender-rules.md``.
13+
14+
Links are absolute GitHub URLs so they open from an installed plugin, a
15+
checkout, or a CLAUDE.md import alike.
816
917
python scripts/build_claude_rules.py # rewrite the file
1018
python scripts/build_claude_rules.py --check # exit 1 if it is stale (CI)
@@ -18,6 +26,14 @@
1826

1927
REPO = Path(__file__).resolve().parent.parent
2028
OUT = REPO / "claude" / "blender-rules.md"
29+
SKILL_OUT = REPO / "claude" / "skills" / "blender-rules" / "SKILL.md"
30+
RULE_URL = "https://github.com/TMHSDigital/Blender-Developer-Tools/blob/main/rules/"
31+
SKILL_FRONT = """---
32+
name: blender-rules
33+
description: Blender Python anti-pattern rules (bmesh leaks, bpy.ops in loops, deprecated context-copy overrides, bulk mesh writes, prop annotations and context guards, Extensions manifest, import scale, export modifiers, glTF/FBX axis RNA). Use when writing, reviewing or fixing any bpy / Blender add-on or script code.
34+
---
35+
36+
"""
2137

2238

2339
def parse(path: Path) -> tuple[str, list[str]]:
@@ -50,24 +66,34 @@ def render() -> str:
5066
"",
5167
desc,
5268
"",
53-
f"Applies to: {', '.join(f'`{g}`' for g in globs) or 'n/a'}. Full rule: `rules/{path.name}`.",
69+
f"Applies to: {', '.join(f'`{g}`' for g in globs) or 'n/a'}. Full rule: [`rules/{path.name}`]({RULE_URL}{path.name}).",
5470
"",
5571
]
5672
return "\n".join(lines).rstrip("\n") + "\n"
5773

5874

75+
def outputs() -> dict[Path, str]:
76+
body = render()
77+
return {OUT: body, SKILL_OUT: SKILL_FRONT + body}
78+
79+
5980
def main() -> int:
60-
new = render()
6181
if "--check" in sys.argv:
62-
old = OUT.read_text(encoding="utf-8").replace("\r\n", "\n") if OUT.exists() else ""
63-
if old != new:
64-
print("claude/blender-rules.md is stale; run python scripts/build_claude_rules.py", file=sys.stderr)
82+
stale = []
83+
for path, new in outputs().items():
84+
old = path.read_text(encoding="utf-8").replace("\r\n", "\n") if path.exists() else ""
85+
if old != new:
86+
stale.append(path.relative_to(REPO).as_posix())
87+
if stale:
88+
print(f"{', '.join(stale)} stale; run python scripts/build_claude_rules.py",
89+
file=sys.stderr)
6590
return 1
66-
print("claude/blender-rules.md is up to date")
91+
print("claude/blender-rules.md and claude/skills/blender-rules/SKILL.md are up to date")
6792
return 0
68-
OUT.parent.mkdir(exist_ok=True)
69-
OUT.write_text(new, encoding="utf-8", newline="\n")
70-
print(f"wrote {OUT.relative_to(REPO)}")
93+
for path, new in outputs().items():
94+
path.parent.mkdir(parents=True, exist_ok=True)
95+
path.write_text(new, encoding="utf-8", newline="\n")
96+
print(f"wrote {path.relative_to(REPO)}")
7197
return 0
7298

7399

0 commit comments

Comments
 (0)