Back to catalogue
ValidationPostToolUse· Write|EditPostToolUseAfter tool execution · non-blocking· non-blocking
Motion rules guard
Animation rule violations flagged as you write them
Enforces the project's Motion (ex-Framer Motion) rules after every component write: no 'framer-motion' imports (use 'motion/react'), no motion.* elements under LazyMotion strict (use m.*), no manual prefers-reduced-motion queries, and no raw width/height/top/left animations. Non-blocking: reports each violation with the expected fix.
What does the Motion rules guard hook do?
Motion rules guard is a Claude Code PostToolUse hook matching Write|Edit. It fires automatically at that lifecycle event — outside the model, so it can't be skipped or forgotten. Animation rule violations flagged as you write them.
Use cases
- Stop an agent from reintroducing motion.* after a context compaction
- Keep every animation on the shared spring/easing language
Tags
#motion#animation#react#conventions
settings.json fragment
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "node $CLAUDE_PROJECT_DIR/.claude/hooks/motion-rules-guard.mjs"
}
]
}
]
}
}Script · .claude/hooks/motion-rules-guard.mjs
#!/usr/bin/env node
// Fait respecter les règles motion du projet après écriture d'un composant
// (PostToolUse Write|Edit). Règles vérifiées (cf. DESIGN.md / CLAUDE.md) :
// - import depuis 'framer-motion' interdit → utiliser 'motion/react'
// - éléments motion.* interdits (LazyMotion strict) → utiliser m.*
// - pas de media query prefers-reduced-motion manuelle (MotionConfig la gère)
// - jamais d'animation brute de width/height/top/left (transform/opacity only)
// Non bloquant : signale les violations avec la correction attendue.
import { readFileSync } from 'fs';
import { fileURLToPath } from 'url';
const RULES = [
[/from\s+['"]framer-motion['"]/, "import 'framer-motion' → use 'motion/react' (paquet `motion`)"],
[/<motion\.\w+/, '<motion.*> crashes under LazyMotion strict → use <m.*> (import { m } from "motion/react")'],
[/prefers-reduced-motion/, 'manual prefers-reduced-motion query → remove it, MotionConfig reducedMotion="user" handles it globally'],
[/animate=\{\{[^}]*\b(?:width|height|top|left)\s*:/s, 'animating width/height/top/left → use transform (x, y, scale) or the `layout` prop'],
];
export function run(input, { readFile = readFileSync } = {}) {
const filePath = input.tool_input?.file_path ?? '';
if (!/\/src\/.*\.[jt]sx?$/.test(filePath)) return null;
let content;
try {
content = readFile(filePath, 'utf8');
} catch {
return null; // fichier illisible/supprimé → rien à vérifier
}
const violations = RULES.filter(([pattern]) => pattern.test(content)).map(([, fix]) => fix);
if (!violations.length) return null;
return {
message:
`[motion-rules] ${filePath} violates the project motion rules:\n` +
violations.map((v) => ` - ${v}`).join('\n') +
'\n→ See CLAUDE.md "Motion / Animations" and src/lib/motion.ts.\n',
};
}
/* v8 ignore next 5 */
if (process.argv[1] === fileURLToPath(import.meta.url)) {
const input = JSON.parse(readFileSync(0, 'utf8'));
const result = run(input);
if (result?.message) process.stderr.write(result.message);
}