This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Division of labor: this file carries repo-specific operational facts an agent needs at runtime — content inventory, Blender runtime discovery, git staging hazards, and the example-shipping quality gates. AGENTS.md carries fleet-standard governance and workflow rules — branching, commits, merge and CI-evidence policy, release automation, authoring standards. Read both; neither repeats the other.
The Blender Developer Tools repository is at v0.158.0. It packages skills, rules, snippets, starter templates, and runnable smoke-gated examples for Blender Python development with Cursor and Claude Code. Coverage targets Blender 5.2 LTS (current stable) with Blender 4.5 LTS fallback. Blender 5.1 is prior stable. The repo ships no MCP server of its own: an opt-in second plugin, blender-live-session (live-session/), registers the official Blender Lab server (see Live MCP vs Headless Harness). Content is consumed directly by the AI when working in Blender add-on or scripting projects.
Version: 0.158.0 License: CC-BY-NC-ND-4.0 Author: TM Hospitality Strategies
skills/<skill-name>/SKILL.md - AI workflow definitions, 25 total
rules/<rule-name>.mdc - Anti-pattern rules, 12 total
templates/<template-name>/ - Starter projects, 3 total
snippets/<snippet-name>.py - Standalone code patterns, 31 total
examples/<name>/ - Runnable smoke-gated examples, 69 total (+ gallery.json)
showcase/<name>/ - Budget-conformance props, 80 pieces, each with a gallery `category` (sibling of examples/; see showcase/README.md § Categories)
scripts/build_gallery.py - Regenerates docs/gallery/ from examples/gallery.json + showcase/gallery.json
scripts/site/ - Vendored landing-page build (Jinja2); tokens.css is the shared palette
tests/check_site_links.py - Internal link/anchor/alt gate over the built site (docs/)
docs/gallery/ - Committed generated gallery pages + hero renders
VERSION - Source of truth for the repo version
| Skill | Purpose |
|---|---|
| addon-scaffolding | Extensions Platform manifest, file layout, register/unregister symmetry |
| ai-mesh-cleanup | Ordered cleanup for imported generated meshes: units, transform apply, origin, normals, budget, collider |
| bake-high-to-low | Cycles cage-bake of high-poly detail onto a low-poly target as a tangent-space normal map |
| engine-export-presets | Unity, Godot and Unreal presets: glTF is +Y up and meters for every engine (export_yup=True; Unreal converts to cm on import), FBX uses axis_forward/axis_up plus centimeter scale |
| operators | bpy.types.Operator lifecycle, bl_idname, redo, defensive context handling |
| ui-panels | bpy.types.Panel declarative draw(), layout primitives, conditional UI |
| custom-properties | bpy.props annotations, PropertyGroup, PointerProperty, storage tradeoffs |
| mesh-editing-and-bmesh | When to use bpy.data vs bpy.ops vs bmesh, foreach_set, depsgraph eval |
| headless-batch-scripting | blender --background --python, temp_override, argparse after -- |
| slotted-actions-animation | Blender 5.x Slotted Actions, channelbag, 4.5 LTS fallback bridge |
| geometry-nodes-python | Programmatic GN tree construction, interface sockets, NODES modifier |
| procedural-materials-and-shaders | Node tree construction for Principled BSDF, emissive, node groups, EEVEE Next vs Cycles |
| depsgraph-and-evaluated-data | evaluated_get / to_mesh / to_mesh_clear lifetime contract for exporters and measurement |
| drivers-and-app-handlers | Driver expressions, driver_namespace, application handlers including the new 5.1 exit_pre |
| bl-info-migration | Three-step migration from legacy bl_info to Extensions Platform, dual-format pattern |
| vse-python | VSE timeline from Python: .strips vs .sequences, new_effect kwargs, 5.2 COLOR width/height bake |
| extension-runtime-and-packaging | Bundled wheels, extension_path_user data that survives upgrades, online_access, bl_ext names, --command extension build in CI |
| timers-modal-and-threading | bpy.app.timers return values and persistent, modal operators on event_timer_add, worker thread + queue + main-thread drain, no timers in --background |
| porting-to-blender-5 | Index of every 4.x → 5.x break the pack covers (bgl, action.fcurves, use_auto_smooth, Principled sockets, .sequences, ID properties, compositor, NodesModifier inputs, ...), each linked to the skill, rule or example that owns the fix; headless triage recipe |
| gpu-drawing-and-overlays | gpu instead of the removed bgl: draw_handler_add/remove, builtin shader names, gpu.state blending and depth, POLYLINE_* line width, blf text, GPUOffScreen with 5.2's gpu.init() headless |
| grease-pencil-v3 | Grease Pencil v3: grease_pencils_v3 (4.5) vs grease_pencils (5.x), layers/frames/drawings, point attributes, Line Art modifier |
| constraints-and-parenting | .parent vs matrix_parent_inverse, stale matrix_world, constraints via constraints.new checked on the evaluated pose, SKT_* socket empties |
| images-and-pixels | Image.pixels layout and bulk foreach_*, byte vs float buffers, colorspace, save() vs save_render(), PNG vs EXR alpha |
| curves-and-text | Curve and TextCurve datablocks: splines, bevel/extrude closed forms, body, Bfont, evaluated mesh read-back |
| compositor-nodes | 4.5 scene.node_tree vs 5.x scene.compositing_node_group, Glare node enum-to-menu-socket change, use_compositing |
Every rule sets alwaysApply: false, so the Scope column below is the rule's
actual load condition, not a description: a rule enters context only when a file
matching its globs does. Changing a glob changes when the rule fires.
| Rule | Scope | What it flags |
|---|---|---|
| prefer-data-over-ops-in-loops | *.py |
bpy.ops.* calls inside iteration over many objects |
| always-free-bmesh | *.py |
bmesh.new() without paired bm.free() in a try/finally block |
| target-extensions-platform-format | Add-on roots | Legacy bl_info only add-ons missing blender_manifest.toml |
| type-annotate-props-and-defend-context | *.py |
bpy.props defined as assignments, unguarded context.active_object |
| prefer-temp-override-over-context-copy | *.py |
bpy.context.copy() passed to operators (deprecated 3.2, removed 4.0) |
| use-foreach-set-for-bulk-data | *.py |
Python loops over mesh.vertices setting bulk attributes one at a time |
| validate-imported-mesh-scale | *.py |
glTF/FBX import then mesh work with no transform_apply and no unit-scale check |
| no-unapplied-modifiers-on-export | *.py |
Export with live modifiers when the export does not request evaluated geometry |
| use-correct-axis-rna-per-exporter | *.py |
export_scene.gltf with FBX axis_forward/axis_up, or export_scene.fbx with glTF export_yup |
| no-bgl | *.py |
import bgl or any bgl. call (removed in 5.x; the add-on fails to register) |
| no-removed-normals-api | *.py |
use_auto_smooth, auto_smooth_angle, calc_normals_split() and the other normals calls removed in 4.1 |
| no-legacy-vse-sequences | *.py |
sequence_editor.sequences / .sequences_all (removed in 5.x; .strips / .strips_all exist on 4.5 too) |
templates/extension-addon-template/ is a copy-paste-ready Blender extension demonstrating:
blender_manifest.tomlwithblender_version_min = "4.5.0"register_classes_factoryregistration pattern- One Operator, one Panel, one PropertyGroup
- A
PointerPropertybound tobpy.types.Scene - Symmetric
register()/unregister()with property cleanup before class unregister
templates/headless-batch-script-template/ is a working starter for unattended Blender batch jobs:
argparseparsing of args after the--separator- Iteration over every mesh object via
bpy.data.objects - Modifier stacks baked with
Mesh.new_from_objecton the evaluated object (no operator, so stack order holds);bpy.context.temp_overrideonly where an operator is genuinely needed - glTF export via
bpy.ops.export_scene.gltf - Explicit exit codes for CI integration
templates/ai-asset-pipeline-template/ is a working starter for a headless GLB-in / engine-ready-out job:
argparseparsing of args after the--separator- Import, unit-scale check, transform apply, origin, normals, evaluated tris
- LOD chain and optional convex/box collider
- Unity / Godot / Unreal glTF export via the engine-export-presets contract
- Explicit exit codes matching
headless-batch-script-template(0, then 2+)
Small standalone .py files at snippets/<name>.py, each 5 to 75 lines.
v0.1.0: canonical object creation and deletion, depsgraph evaluated mesh, bmesh load-edit-free, temp_override context, foreach_set vertex bulk write, register_classes_factory, PointerProperty binding, cross-version property delete, and the action_ensure_channelbag_for_slot slotted-actions bridge.
v0.2.0: Principled BSDF material, driver-with-custom-function via driver_namespace, application handler registration, shader node group with cross-version interface API, foreach_get bulk vertex read, version-branch skeleton, and USD export with evaluation_mode='RENDER'.
AI asset pipeline track: decimate_to_budget.py, convex_hull_collider.py, lod_chain.py (helper duplicated, not imported), gltf_draco_export.py, export_preset_unity.py, export_preset_godot.py, export_preset_unreal.py, setup_bake_target_image.py, bake_normal_high_to_low.py, save_baked_image.py.
Extensions runtime: extension-user-data.py (extension_path_user settings file and an online_access guard). Long-running work: thread-queue-timer.py (worker thread, queue.Queue, main-thread drain timer). Porting to 5.x: gpu-draw-handler.py (viewport overlay with a stored, removable draw handler) and set-sharp-from-angle.py (the 4.1+ replacement for use_auto_smooth).
Runnable scripts at examples/<name>/, each asserting a real API contract with
deterministic checks (exit non-zero on failure) and optionally rendering a still via
--output. All of them run headless on Blender 5.2 LTS and 4.5 LTS in blender-smoke.yml (5.1 on the weekly cron, the needs-5.1 PR label, or manual dispatch);
56 of the 69 ship a render in the site gallery at docs/gallery/. The other
thirteen are check-only: they carry no --output path, no gallery entry, and no
hero asset. The criterion is whether the contract is expressible in pixels. An
example is check-only when its witness is a data or state fact that no scene
redesign can make visible — a datablock name, an RNA attribute's presence, a
post-exit sidecar, a topology count, export metadata — so a render would look
identical whether the API held or broke. That is a category, not a shortcut: a
subject whose render could be made legible must be rendered, per
docs/new-example-prompt.md. Smoke coverage is unaffected; every check-only
example still carries a tests/smoke/catalog.json row and a falsifier.
examples/gallery.json is the
gallery's source of truth. When authoring a new gallery example, copy the anatomy of
examples/bmesh-gear/ (script structure, README shape, dark-studio render recipe) and
wire all of: gallery.json entry (with an alt sentence describing the still; the build fails without one),
a tests/smoke/catalog.json row, an examples/skills.json entry naming the skills it proves (python scripts/build_examples_index.py then regenerates examples/index.json and each skill's Runnable examples section; Validate fails on a missing entry), hero webp (1280×720) in
docs/gallery/assets/ + its 640×360 card variant <name>-hero-640.webp (python scripts/make_thumbs.py; the gallery build exits 5 without it) + preview webp (1200×675), then run python scripts/build_gallery.py.
A check-only example wires only the
tests/smoke/catalog.json row, its examples/skills.json entry, and its README — it is deliberately absent from
examples/gallery.json and docs/gallery/.
Renders must conform to the gallery look spec at docs/VISUAL-STYLE.md.
Render paths gate framing through the shared helper examples/gallery_framing.py —
imported via a __file__-relative sys.path shim, the repo's only
cross-example import mechanism (see its docstring).
- Local Blender binaries: check
.scratch/at the repo root first — some machines have no system Blender install, and a prior agent run downloads official releases there (e.g..scratch/5.2/blender-5.2.x-.../blender[.exe]). Then check system installs. Do not probe blindly; locate the binary, run it, and state the exact binary path and the version the binary itself reports in every report. - 5.2 LTS is the local check version. 4.5 LTS is exercised by CI when unavailable locally. 4.4 is not a substitute for 4.5 and must never be reported as 4.5.
- CI floats within each series:
blender-smoke.ymlresolves the highest published point release at run time (sort -V | tail -1on the download listing), so a local 4.5.x may lag CI — its 4.5 job ran 4.5.12 LTS on PR #107 while local.scratch/held 4.5.11. Either way, state the exact version the binary reports. - If
.scratch/lacks a needed version, download an official release from download.blender.org into it..scratchis gitignored. - In scripts, version-branch on the
bpy.app.versiontuple, never onbpy.app.version_string— it reads e.g."4.5.11 LTS", not bare semver.
Two execution paths exist for Blender work. The headless harness (.scratch/ or system binaries, check-only scripts, CI smoke) is the only source of truth for correctness — every witness, check, and CI gate runs headless. The official Blender Lab MCP server (lab/blender_mcp), when registered on the local machine, provides a live interactive session for exploration. The opt-in blender-live-session plugin ships it (#483): live-session/.claude-plugin/plugin.json and live-session/.cursor-plugin/plugin.json set "mcpServers": "./mcp.json", which runs uvx --exclude-newer <date> --from git+https://projects.blender.org/lab/blender_mcp.git@<sha>#subdirectory=mcp blender-mcp; the core plugin must not declare a server. tests/check_manifest_meta.py requires a full 40-hex commit pin and --exclude-newer, build_plugin_dist.py ships live-session/ and fails if a manifest's path is missing, and mcp-server.yml runs tests/check_mcp_server.py (install + initialize + tools/list over stdio) weekly and on every change to live-session/. To bump it, review the upstream diff, replace the SHA, set --exclude-newer to the day after that commit, and run python tests/check_mcp_server.py. The plugin's live-blender-session skill carries this policy to plugin users. The server is GPL-3.0: reference it, never vendor it. The add-on needs Blender 5.1+.
- Use the live MCP for: interactively inspecting a scene while designing a render, querying data relations, diagnosing why staged geometry or lighting looks wrong, and prototyping API calls before committing them to a script.
- Never use the live MCP for: anything a check depends on, anything reported as verification evidence, or anything CI must reproduce. A behavior observed in the live session must be re-proven in the headless script before it counts. Examples must never import from, depend on, or assume the MCP server.
- Safety: the server executes generated code in the running Blender instance without guards. Only attach it to scratch scenes, never to files you cannot afford to mutate.
- Availability is machine-local and optional: if the server is not registered, proceed headless without comment.
Stage with explicit paths only — never git add -A or git add .. Cursor agent sessions inject a local guidance block into the working-copy CLAUDE.md; a bulk add sweeps it into the commit. It manifests as a CLAUDE.md hunk in git diff that you did not author (the file can be dirty before you touch anything). Leave it unstaged, and never stage CLAUDE.md unless you deliberately edited it.
docs/VISUAL-STYLE.mdis the binding render standard; deviations are defects.- Framing gate: Layer 1 framing is measured, not eyeballed — the example's
--outputrender path calls the shared helperexamples/gallery_framing.py(check_framing, exit 10 on violation) before writing the still: hero fill 0.70–0.90 of frame in at least one axis, every element that matters clearing all four edges by ≥ 0.02. The check-only path never invokes it, so smoke runtimes are unaffected. The helper measures; call sites enforce. Bleed compositions callmeasure_framing_deviationand assert their own cap at the call site —check_framinghas nodeviation=flag. - Contact-sheet gate: composite the candidate hero beside the pinned calibration set — currently
armature-bend,damped-track-aim,bmesh-gear— commit the composite underdocs/gallery/contact-sheets/, link it in the PR body, and report per-criterion verdicts (stage darkness, wedge warmth, subject fill, saturation, thumbnail legibility) including mean luminance versus the calibration images. A claim without the committed composite is not acceptable evidence. Re-rendering a hero means regenerating its sheet:tests/check_contact_sheets.py(Validate) fails when a sheet's candidate tile no longer matches the hero that ships (#487), and an asset-type piece's asset sheet must be re-rendered in the same commit. This list is the canonical home of the pinned set — update it here when a new example outclasses a member;docs/new-example-prompt.mdpoints here rather than naming members. The longer "calibration references" list indocs/VISUAL-STYLE.mdis a style reference, not this contact-sheet set. Tooling:python scripts/render_hero.py --blender PATH --only NAMErenders the README--outputcommand and writes the hero, its 640 card variant, and the preview webps (the one committed PNG-to-webp path);python scripts/contact_sheet.py NAMEbuilds the composite in the committed format and prints each tile's luma, stage, wedge warmth and saturation;python scripts/measure_hero_drift.pychecks committed heroes against what the code renders now. - Asset-sheet gate (asset-type examples — game props/kits): composite the hero asset rendered alone (neutral three-quarter view, plain studio lighting, no staging tricks, no labels, no comparison props) beside the pinned asset-quality reference set — currently
collision-hull-proxy,socket-attach-points,vertex-color-ao,wheelbarrow,apothecary-shelf— rendered the same way; commit underdocs/gallery/asset-sheets/, link it in the PR body, and report a verdict. The asset ships only if it is not identifiable as the least-designed object in that lineup — a strong scene can carry a weak model; this gate removes the scene. This list is the canonical home of the reference set — update it here when a new asset outclasses a member. A reference may be an example or a showcase piece. Tooling:python scripts/asset_sheet.py --blender PATH NAMErenders the candidate and this set (read from this line) throughscripts/asset_sheet_panel.pyand writes the composite. The measurable floors behind the gate (naming, material variation, edge treatment) live inexamples/gallery_asset_quality.py— render path only, same call pattern asgallery_framing, exit 11 on violation — with the calibration table and dropped-floor evidence indocs/VISUAL-STYLE.md§ Asset quality. - Falsification: every check must be proven to fail once — break the contract, observe the non-zero exit, restore — with the probe and the measured error reported in the PR body. An assertion that cannot fail witnesses nothing.
- After gallery regeneration (
python scripts/build_gallery.py), read the generated HTML character by character — the<img alt>text and witnesses callouts indocs/gallery/index.htmlanddocs/gallery/<name>/index.html— not justexamples/gallery.json. Precedent: theteaches.split(".")[0]bug truncated 14/21 card alts at dotted API paths likebmesh.opswhile the source JSON looked fine (fixed in PR #68). - Playwright gallery captures: gallery
<img>tags lazy-load, so force them first (document.querySelectorAll('img').forEach(i => i.loading = 'eager'), then wait). Scroll the target card into view and take a viewport capture —scrollIntoView({block:'center', behavior:'instant'}), short wait,browser_take_screenshotwithfullPageomitted. AfullPagecapture is NOT a workaround: on a tall gallery page it renders every card image blank even when the images are verified loaded (complete === true,naturalWidth === 1280,opacity === 1) — measured on the 45-card grid at 1425x4516. Verify load state viabrowser_evaluaterather than trusting the pixels.
The host runner is tests/smoke/run_example.py. Shipped examples are listed in
tests/smoke/catalog.json (not a new YAML step per example).
- SKIP: print
SMOKE_SKIP: <reason>andsys.exit(77). Legal only when the catalog/runner--min-versionis above this Blender. Exit 0 with that marker is FAIL (vacuous). Skip on a version that should run is FAIL. - Post-exit sidecar: opt-in
--expect-sidecar PATH. The example writes$BDT_SMOKE_SIDECAR(set by the runner). The harness asserts after Blender exits. Not a gallery still. - Falsifiers in CI: every catalog row carries
falsifiers, at least one{"args": [...], "expect_exit": N}(N not 0, 1, 2 or 77; 2 is argparse usage) or, for a contract the sidecar check owns,{"args": [...], "expect_sidecar_fail": true}. A falsifier that targets a version-specific API change takes"min_version": "5.2"and is recorded as SKIP below it (its README must say it exits 0 there).run_catalog.pyruns each after the row's happy path; it passes only on exactly that exit.tests/check_smoke_catalog.pyrejects a row without one and any unknown key. The weekly cron also runs the full showcase sweep (tests/check_falsifier_targets.py --run --shard K/4, four jobs; dispatch withsweep: trueto run it by hand), and a red or cancelled weekly run opens or comments on a "Weekly Blender smoke run failed" issue. - Content smoke (#447):
content-smoke.ymlruns everysnippets/*.pyin place (its check and falsifier live intests/smoke/snippet_checks.py) and every SKILL.md ```python block tagged<!-- smoke: run -->, via `tests/smoke/run_content.py`, on 5.2 and 4.5. It has a positive `paths` filter because blender-smoke ignores `**.md`; `release-gate.sh` waits for it on pushes touching skills or snippets. Validate's `tests/check_content_catalog.py` compiles every SKILL.md block and requires a catalog entry per snippet. - Summary:
tests/smoke/summarize.pyprints passed/skipped/failed. Zero PASSes makes the job red even if every example skipped cleanly. Falsifier runs are tabled separately and never count toward that PASS.
- The canonical example-creation prompt lives at
docs/new-example-prompt.md; keep it in agreement with this file andAGENTS.md. - The
ROADMAP.md"Candidate pool" section is the subject source for example runs: remove a subject when it ships, and restock with subjects identified but not built. - PR bodies label what was proven by live run versus established by inspection only (see
AGENTS.md§ Merge policy and CI evidence for the merge and post-merge verification rules).
This is a content repository, no build step for skills/rules/snippets/templates — edit
SKILL.md, .mdc, .py, and .toml files directly. The website is generated:
scripts/build_gallery.py (stdlib) regenerates docs/gallery/ and must be re-run after
touching examples/; the landing page builds from scripts/site/ at deploy time.
- One palette:
scripts/site/tokens.cssis the single:rootfor every published page — the landing and 404 templates{% include %}it andbuild_gallery.pyinlines it. Change colors or type there, never in a hand-copied:root; a token edit changes every gallery page, so regenerate and commitdocs/gallery/. - Link gate: after building both (
python scripts/site/build_site.py --repo-root . --out docsthenpython scripts/build_gallery.py), runpython tests/check_site_links.py. It resolves every internalhref/src/#anchoracross all pages and requiresalton every<img>; thevalidate-sitejob and the deploy both run it. Gallery hash fragments with=(#tag=a,b,#k=showcase,#s=az) are filter state, not anchors, and are skipped. - "What's new" needs history: the landing page dates recent additions from
git log; a shallow clone hides the strip, which is whypages.ymlchecks out withfetch-depth: 0.
The AI consumes content via:
- Cursor: rules under
rules/apply when scope globs match. Skills are referenced by name in chat. - Claude Code: install the plugin (
.claude-plugin/, README Quick start) for the skills. Claude Code does not read.mdcrules; it gets them throughclaude/blender-rules.md, generated fromrules/byscripts/build_claude_rules.py.
- Blender versions: 5.2 LTS primary, 5.1 prior stable, 4.5 LTS fallback. Skills must show both code paths when 4.x and 5.x APIs diverge, including the 5.1-vs-5.2 NodesModifier input split and the 5.2 COLOR strip
width/heightbake. - Properties as annotations:
my_prop: bpy.props.FloatProperty(...)(correct), notmy_prop = bpy.props.FloatProperty(...)(deprecated). - bmesh memory: every
bmesh.new()must be paired withbm.free()in atry/finally. - No
bpy.opsin tight loops: usebpy.data.*andbmeshfor bulk work. - Extensions over
bl_info: new add-ons ship as Extensions withblender_manifest.toml.bl_infomay appear alongside as a fallback only. - Defensive context: any code touching
context.active_objectmust guard withif obj is None: return.
| Area | URL |
|---|---|
| Python API (5.2 LTS) | https://docs.blender.org/api/current/ |
| Python API (5.1) | https://docs.blender.org/api/5.1/ |
| Python API (4.5 LTS) | https://docs.blender.org/api/4.5/ |
| Extensions Platform | https://docs.blender.org/manual/en/latest/advanced/extensions/index.html |
| Release notes | https://developer.blender.org/ |
The release pipeline is automated via release.yml on every push to main; it releases only for feat:/fix:/breaking commit subjects and only after release-gate.sh sees green CI. The release-doc-sync@v1 step rewrites CHANGELOG.md, this CLAUDE.md **Version:** line, and ROADMAP.md **Current:** line on each release. Never hand-edit those lines, the action owns them.
When adding content to a future version:
- Add files under
skills/,rules/,snippets/,templates/, orexamples/. - Update README.md aggregate counts (the
validate-countsjob enforces correctness). The GitHub repo description deliberately carries no counts: nothing can check it, and it went stale twice (#448, #486). Keep numbers out of it. - Update ROADMAP.md candidate pool entries.
- Use
feat:for new content,fix:for corrections. - Push. The release pipeline handles VERSION, tags, CHANGELOG, the
**Version:**line here, and the**Current:**line in ROADMAP.md.