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)
error · VRChat rejects it or the avatar breaks warn · likely a problem info · advisory

The rules

CodeSeverityRule
VRC001warnVRChat avatar SDK (com.vrchat.avatars) not found in vpm-manifest.json
VRC002infoNo VRC Avatar Descriptor found under Assets/ (only when the avatar SDK is present)
VRC010errorSynced expression parameters exceed the 256-bit budget (Bool = 1, Int/Float = 8; only networkSynced count)
VRC011warnDuplicate expression parameter name within one asset
VRC012infoAn 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
VRC020errorExpression menu has more than 8 controls
VRC021warnMenu control references a parameter not declared anywhere (and not built-in); may be build-time generated
VRC022warnA menu control drives nothing — no parameter, no subParameters, and no sub-menu
VRC030warnCustom expressions but the Expression Parameters reference is unassigned/missing/wrong type
VRC031warnAs VRC030, for the Expression Menu reference
VRC032warnA custom playable layer references an animator controller whose guid is absent
VRC033warnViseme Blend Shape lip-sync but the viseme mesh is unassigned, or blend-shape count ≠ 15
VRC034warnEye Look enabled but no eye bones assigned
VRC035warnEyelid Type is Blendshapes but no eyelid skinned mesh assigned
VRC036warnAn Expression Parameter is used by none of the resolvable non-default playable-layer controllers
VRC037warnAn Expression Parameter shares a name with an animator parameter but an incompatible type
VRC038warnWith the viseme entry count correct (15), an entry is empty/-none-, or two entries name the same blend shape — complements VRC033
VRC039warnA 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
VRC040warnA transition condition references an undeclared animator parameter (transition silently never fires)
VRC041warnA blend tree reads an undeclared animator parameter (respects 1D/2D/Direct blend type)
VRC042warnA state machine has child states but no default state (never enters any state)
VRC043warnDuplicate animator parameter name within a controller
VRC044warnStates in one controller mix Write Defaults on and off (common cause of broken/sticky animations)
VRC045warnWrite Defaults inconsistent across the avatar's resolvable non-default playable-layer controllers — the avatar-level counterpart to VRC044
VRC046warnA state's (or blend-tree child's) motion references a guid not in the project — clip moved/deleted, the state silently plays nothing
VRC047warnA clip played by the FX playable layer animates transform or humanoid-muscle curves (FX is for non-transform animation). FBX-embedded clips are skipped
VRC048infoA state has no Motion assigned (plays nothing). Advisory — empty states are a common intentional idiom (e.g. a WD-off buffer state)
VRC049infoAn animation clip has no curves — a no-op asset, usually an authoring slip
VRC050warnA PhysBone's root transform can't be resolved in the file (e.g. stripped from a nested prefab)
VRC051warnA PhysBone's root resolves but it moves zero transforms (no child bones, no endpoint) — it simulates nothing
VRC052warnA PhysBone's colliders list has slots but every slot is a null reference (an empty colliders: [] is fine)
VRC060warnA MonoBehaviour's m_Script is null — Unity's "Missing (Mono Script)". The SDK's build validation refuses to upload with missing scripts
VRC061infoA 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
VRC062warnA 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.

A Unity guid is 32 hex characters and is parsed as a string; a reference with only a local 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 (VRC046VRC049); 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.