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.
As a PostToolUse hook it runs after the action, reacting to what just happened rather than blocking it. Because it is a deterministic Node.js script, it executes on every matching event without relying on the model to remember — the guarantee that makes agentic workflows safe to automate.
Use cases
- Stop an agent from reintroducing motion.* after a context compaction
- Keep every animation on the shared spring/easing language
Tags
settings.json fragment
{
"hooks": {
"PostToolUse": [
{
"hooks": [
{
"command": "node $CLAUDE_PROJECT_DIR/.claude/hooks/motion-rules-guard.mjs",
"type": "command"
}
],
"matcher": "Write|Edit"
}
]
}
}Script · .claude/hooks/motion-rules-guard.mjs
#!/usr/bin/env node
// @hookstack motion-rules-guard
// 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 "node:fs";
import { fileURLToPath } from "node: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);
}