Claude-skill-registry fvtt-hooks
This skill should be used when registering hooks, creating custom hooks for module APIs, debugging hook execution, handling async in hooks, or preventing memory leaks from unclean hook removal. Covers Hooks.on/once/call/callAll, lifecycle order, and common pitfalls.
git clone https://github.com/majiayu000/claude-skill-registry
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/fvtt-hooks" ~/.claude/skills/majiayu000-claude-skill-registry-fvtt-hooks && rm -rf "$T"
skills/data/fvtt-hooks/SKILL.mdFoundry VTT Hooks
Domain: Foundry VTT Module/System Development Status: Production-Ready Last Updated: 2026-01-04
Overview
Foundry VTT uses an event-driven architecture where hooks allow modules to intercept and respond to VTT workflows. Understanding hooks is fundamental to all Foundry development.
When to Use This Skill
- Registering event handlers for document changes
- Creating module APIs that other packages can consume
- Debugging hook execution order
- Preventing memory leaks from orphaned hooks
- Understanding async limitations in hooks
Core Concepts
| Method | Purpose | Cancellable | Persists |
|---|---|---|---|
| Register persistent listener | N/A | Yes |
| Register one-time listener | N/A | No |
| Trigger hook (stoppable) | Yes | N/A |
| Trigger hook (all run) | No | N/A |
| Unregister by ID | N/A | N/A |
Lifecycle Hooks (Execution Order)
These fire once per client connection, in this order:
init → i18nInit → setup → ready
| Hook | When | Use For |
|---|---|---|
| Foundry initializing | Register sheets, settings, CONFIG |
| Translations loaded | Localization-dependent setup |
| Before Documents/UI/Canvas | Pre-game state setup |
| Game fully ready | World initialization, game data access |
// Correct registration order Hooks.once("init", () => { // Register settings game.settings.register("my-module", "mySetting", { /* ... */ }); // Register document sheets DocumentSheetConfig.registerSheet(Actor, "my-module", MyActorSheet, { makeDefault: true }); }); Hooks.once("ready", () => { // Safe to access game.actors, game.users, etc. console.log(`${game.actors.size} actors in world`); });
Document Hooks
All document types follow this pattern:
| Hook | Trigger Method | Can Cancel |
|---|---|---|
| | Yes |
| | No |
| | Yes |
| | No |
| | Yes |
| | No |
// Prevent update if condition met Hooks.on("preUpdateActor", (actor, change, options, userId) => { if (change.system?.hp?.value < 0) { ui.notifications.warn("HP cannot be negative"); return false; // Cancels the update } }); // React after update Hooks.on("updateActor", (actor, change, options, userId) => { if (change.system?.hp?.value === 0) { ChatMessage.create({ content: `${actor.name} has fallen!` }); } });
Render Hooks
Render hooks fire for the entire inheritance chain:
// renderActorSheet → renderDocumentSheet → renderFormApplication → renderApplication // ApplicationV1 signature Hooks.on("renderActorSheet", (app, html, data) => { html.find(".header").append("<button>Custom</button>"); }); // ApplicationV2 signature (V12+) Hooks.on("renderActorSheetV2", (app, html, context, options) => { // html is the rendered element });
Common Pitfalls
1. Async Functions Cannot Cancel Hooks
CRITICAL: Hooks never await callbacks. Async functions return a Promise, not
false.
// WRONG - Won't prevent the update! Hooks.on("preUpdateActor", async (actor, change, options, userId) => { const allowed = await someAsyncCheck(); if (!allowed) return false; // Returns Promise, not false! }); // CORRECT - Synchronous check for cancellation Hooks.on("preUpdateActor", (actor, change, options, userId) => { if (!someCondition) return false; // Actually prevents update });
2. Memory Leaks from Orphaned Hooks
Hooks keep references to callbacks, preventing garbage collection:
// BAD - Hook persists after app closes class MyApp extends Application { constructor() { super(); Hooks.on("updateActor", this._onUpdate.bind(this)); } } // GOOD - Store ID and clean up class MyApp extends Application { constructor() { super(); this._hookId = Hooks.on("updateActor", this._onUpdate.bind(this)); } close(options) { Hooks.off("updateActor", this._hookId); return super.close(options); } }
3. Object Mutation vs Re-assignment
Objects are passed by reference - mutation works, re-assignment doesn't:
// WORKS - Mutation affects original Hooks.on("preUpdateActor", (actor, change, options, userId) => { change.name = "Modified"; // Changes the actual update }); // DOESN'T WORK - Re-assignment breaks reference Hooks.on("preUpdateActor", (actor, change, options, userId) => { change = { name: "New" }; // Local variable only! });
4. Hooks Fire Per-Client
Each client runs hooks independently. Check userId to avoid duplicate actions:
// BAD - All clients try to create the item Hooks.on("createActor", (actor, options, userId) => { actor.createEmbeddedDocuments("Item", [itemData]); }); // GOOD - Only triggering client acts Hooks.on("createActor", (actor, options, userId) => { if (userId !== game.user.id) return; actor.createEmbeddedDocuments("Item", [itemData]); });
5. Hook Context (this) Issues
// WRONG - Can't unregister by function reference Hooks.on("updateActor", this.onUpdate); Hooks.off("updateActor", this.onUpdate); // Fails! // WRONG - bind() creates new function each time Hooks.on("updateActor", this.onUpdate.bind(this)); Hooks.off("updateActor", this.onUpdate.bind(this)); // Different function! // CORRECT - Use hook ID this._hookId = Hooks.on("updateActor", this.onUpdate.bind(this)); Hooks.off("updateActor", this._hookId); // Works
Creating Custom Hooks for Module APIs
Basic Pattern
// my-module.js Hooks.once("init", () => { const api = { registerExtension: (config) => { /* ... */ }, doAction: (data) => { /* ... */ } }; // Expose API game.modules.get("my-module").api = api; // Announce readiness Hooks.callAll("myModuleReady", api); }); // Other modules consume it Hooks.once("myModuleReady", (api) => { api.registerExtension({ name: "My Extension" }); });
Allowing Cancellation
class MySystem { static performAction(data) { // Allow other modules to prevent action const allowed = Hooks.call("myModule.beforeAction", data); if (allowed === false) return null; const result = this._doWork(data); // Notify completion (cannot be cancelled) Hooks.callAll("myModule.afterAction", result); return result; } }
Naming Convention
Use namespaced names:
moduleName.eventName
combatTracker.turnChangedtokenMagic.effectAppliedmyModule.ready
Debugging Hooks
Enable Debug Mode
// In browser console CONFIG.debug.hooks = true; // Toggle macro CONFIG.debug.hooks = !CONFIG.debug.hooks; console.warn("Hook debugging:", CONFIG.debug.hooks);
Debug Specific Hook
// Log arguments for a specific hook once Hooks.once("updateActor", (...args) => console.log("updateActor args:", args));
Developer Mode Module
Use the Developer Mode module for persistent debug flag management without shipping debug code.
Implementation Checklist
- Use
for init/setup/ready (notHooks.once
)Hooks.on - Store hook IDs for any persistent hooks
- Clean up hooks in
or destruction methodsclose() - Check
before document operationsuserId === game.user.id - Never use async for
hooks that need cancellationpre* - Mutate objects, don't re-assign them
- Use namespaced names for custom hooks
- Use
for cancellable events,Hooks.call
for notificationsHooks.callAll
Quick Reference: Common Hook Signatures
// Document hooks Hooks.on("preCreateActor", (document, data, options, userId) => {}); Hooks.on("createActor", (document, options, userId) => {}); Hooks.on("preUpdateActor", (document, change, options, userId) => {}); Hooks.on("updateActor", (document, change, options, userId) => {}); Hooks.on("preDeleteActor", (document, options, userId) => {}); Hooks.on("deleteActor", (document, options, userId) => {}); // Render hooks (V1) Hooks.on("renderActorSheet", (app, html, data) => {}); // Render hooks (V2) Hooks.on("renderActorSheetV2", (app, html, context, options) => {}); // Canvas hooks Hooks.on("canvasReady", (canvas) => {}); Hooks.on("canvasPan", (canvas, position) => {});
References
- Hooks API Documentation
- Hook Events Reference
- Community Wiki: Hooks
- Community Wiki: Hooks Listening & Calling
Last Updated: 2026-01-04 Status: Production-Ready Maintainer: ImproperSubset