SDK3 lint rules
Rules emitted by avatar lint <project> (the avatar-lint crate). Errors are
things VRChat will reject or that break the avatar; warnings are likely-but-not-certain problems. Codes
are grouped: VRC00x project, VRC01x parameters, VRC02x menus,
VRC03x Avatar Descriptor, VRC04x animator controllers + animation clips,
VRC05x PhysBones / Avatar Dynamics, VRC06x project hygiene / Android (Quest).
Full text: docs/reference/sdk3-lint-rules.md.
Running it
avatar lint path/to/UnityProject # report; exit 1 on errors
avatar lint path/to/UnityProject --deny-warnings # exit 1 on warnings too
avatar lint path/to/UnityProject --json # LintReport (avatar schema lint)The rules
| Code | Severity | Rule |
|---|---|---|
VRC001 | warn | VRChat avatar SDK (com.vrchat.avatars) not found in vpm-manifest.json |
VRC002 | info | No VRC Avatar Descriptor found under Assets/ (only when the avatar SDK is present) |
VRC010 | error | Synced expression parameters exceed the 256-bit budget (Bool = 1, Int/Float = 8; only networkSynced count) |
VRC011 | warn | Duplicate expression parameter name within one asset |
VRC012 | info | An Expression Parameter is referenced by no menu control, animator condition, or blend tree anywhere in the project — a project-wide superset of VRC036. Advisory (the name can be supplied by OSC/contacts/Modular Avatar/VRCFury); built-ins and default-layer params excluded |
VRC020 | error | Expression menu has more than 8 controls |
VRC021 | warn | Menu control references a parameter not declared anywhere (and not built-in); may be build-time generated |
VRC022 | warn | A menu control drives nothing — no parameter, no subParameters, and no sub-menu |
VRC030 | warn | Custom expressions but the Expression Parameters reference is unassigned/missing/wrong type |
VRC031 | warn | As VRC030, for the Expression Menu reference |
VRC032 | warn | A custom playable layer references an animator controller whose guid is absent |
VRC033 | warn | Viseme Blend Shape lip-sync but the viseme mesh is unassigned, or blend-shape count ≠ 15 |
VRC034 | warn | Eye Look enabled but no eye bones assigned |
VRC035 | warn | Eyelid Type is Blendshapes but no eyelid skinned mesh assigned |
VRC036 | warn | An Expression Parameter is used by none of the resolvable non-default playable-layer controllers |
VRC037 | warn | An Expression Parameter shares a name with an animator parameter but an incompatible type |
VRC038 | warn | With the viseme entry count correct (15), an entry is empty/-none-, or two entries name the same blend shape — complements VRC033 |
VRC039 | warn | A viseme blendshape name doesn't exist on the viseme mesh's source FBX (its morph channels) — the descriptor↔mesh cross-layer check; silently broken lip-sync in-game. Unresolvable steps return quietly |
VRC040 | warn | A transition condition references an undeclared animator parameter (transition silently never fires) |
VRC041 | warn | A blend tree reads an undeclared animator parameter (respects 1D/2D/Direct blend type) |
VRC042 | warn | A state machine has child states but no default state (never enters any state) |
VRC043 | warn | Duplicate animator parameter name within a controller |
VRC044 | warn | States in one controller mix Write Defaults on and off (common cause of broken/sticky animations) |
VRC045 | warn | Write Defaults inconsistent across the avatar's resolvable non-default playable-layer controllers — the avatar-level counterpart to VRC044 |
VRC046 | warn | A state's (or blend-tree child's) motion references a guid not in the project — clip moved/deleted, the state silently plays nothing |
VRC047 | warn | A clip played by the FX playable layer animates transform or humanoid-muscle curves (FX is for non-transform animation). FBX-embedded clips are skipped |
VRC048 | info | A state has no Motion assigned (plays nothing). Advisory — empty states are a common intentional idiom (e.g. a WD-off buffer state) |
VRC049 | info | An animation clip has no curves — a no-op asset, usually an authoring slip |
VRC050 | warn | A PhysBone's root transform can't be resolved in the file (e.g. stripped from a nested prefab) |
VRC051 | warn | A PhysBone's root resolves but it moves zero transforms (no child bones, no endpoint) — it simulates nothing |
VRC052 | warn | A PhysBone's colliders list has slots but every slot is a null reference (an empty colliders: [] is fine) |
VRC060 | warn | A MonoBehaviour's m_Script is null — Unity's "Missing (Mono Script)". The SDK's build validation refuses to upload with missing scripts |
VRC061 | info | A renderer material uses a shader not in VRChat's Android/Quest mobile whitelist (VRChat/Mobile/*) — fine PC-only, blocks a Quest upload. Conservative: only positively-identified non-mobile shaders |
VRC062 | warn | A custom-layer animation drives a blendshape the descriptor registers as an eyelid — VRChat's eyelid driver writes registered shapes after the animators every frame, so the animation is silently overridden (the classic "my blink expression does nothing"). Fires regardless of enableEyeLook; resolves eyelid indices through the source FBX's morph channels in import order |
How assets are identified
VRChat assets are recognized structurally, not by a hardcoded script GUID: an Expression
Parameters asset has a parameters list whose entries carry valueType/name;
an Expression Menu has a controls list; an Avatar Descriptor MonoBehaviour carries
baseAnimationLayers + ViewPosition; a PhysBone is a MonoBehaviour carrying
endpointPosition or multiChildType (the same test avatar stats uses).
This keeps the linter working across SDK versions. .asset, .prefab, and
.unity are all scanned; cross-asset references are resolved through a guid→path index
built from .meta files.
fileID (no guid) can't be resolved across files and is skipped rather than flagged.
.controller and .anim files are parsed into typed readers by
avatar-unity-asset; the VRC04x rules aggregate fields by Unity class id, which
keeps them robust to SDK drift.
Coverage and cross-checks
Current scope: Expression Parameters/Menus; the Avatar Descriptor in prefabs/scenes (expression +
playable-layer references, viseme lip-sync incl. per-entry checks and the viseme↔source-FBX
morph-channel cross-check, eye-look config, avatar-level Write-Defaults consistency); animator
controllers; animation clips (VRC046–VRC049); PhysBones / Avatar Dynamics
(VRC05x); and project hygiene / Android (VRC06x). Not yet: contacts.
The VRC036/037 cross-check resolves the descriptor's playable-layer controllers
by guid (skipping isDefault layers, which use VRChat's built-in controllers) and compares the
union of their declared parameters against the avatar's Expression Parameters — a name with no match
is VRC036, a name matching with a mismatched type is VRC037; the check is skipped
when no controller resolves. valueType in VRCExpressionParameters:
0 Int, 1 Float, 2 Bool.