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.
Registers event handlers and creates custom hooks for Foundry VTT modules. Used when handling document lifecycle events, preventing memory leaks, or debugging hook execution order.
/plugin marketplace add ImproperSubset/hh-agentics/plugin install fvtt-dev@hh-agenticsThis skill inherits all available tools. When active, it can use any tool Claude has access to.
Domain: Foundry VTT Module/System Development Status: Production-Ready Last Updated: 2026-01-04
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.
| Method | Purpose | Cancellable | Persists |
|---|---|---|---|
Hooks.on(name, fn) | Register persistent listener | N/A | Yes |
Hooks.once(name, fn) | Register one-time listener | N/A | No |
Hooks.call(name, ...args) | Trigger hook (stoppable) | Yes | N/A |
Hooks.callAll(name, ...args) | Trigger hook (all run) | No | N/A |
Hooks.off(name, id) | Unregister by ID | N/A | N/A |
These fire once per client connection, in this order:
init → i18nInit → setup → ready
| Hook | When | Use For |
|---|---|---|
init | Foundry initializing | Register sheets, settings, CONFIG |
i18nInit | Translations loaded | Localization-dependent setup |
setup | Before Documents/UI/Canvas | Pre-game state setup |
ready | 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`);
});
All document types follow this pattern:
| Hook | Trigger Method | Can Cancel |
|---|---|---|
preCreate{Doc} | Hooks.call | Yes |
create{Doc} | Hooks.callAll | No |
preUpdate{Doc} | Hooks.call | Yes |
update{Doc} | Hooks.callAll | No |
preDelete{Doc} | Hooks.call | Yes |
delete{Doc} | Hooks.callAll | 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 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
});
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
});
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);
}
}
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!
});
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]);
});
// 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
// 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" });
});
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;
}
}
Use namespaced names: moduleName.eventName
combatTracker.turnChangedtokenMagic.effectAppliedmyModule.ready// In browser console
CONFIG.debug.hooks = true;
// Toggle macro
CONFIG.debug.hooks = !CONFIG.debug.hooks;
console.warn("Hook debugging:", CONFIG.debug.hooks);
// Log arguments for a specific hook once
Hooks.once("updateActor", (...args) => console.log("updateActor args:", args));
Use the Developer Mode module for persistent debug flag management without shipping debug code.
Hooks.once for init/setup/ready (not Hooks.on)close() or destruction methodsuserId === game.user.id before document operationspre* hooks that need cancellationHooks.call for cancellable events, Hooks.callAll for notifications// 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) => {});
Last Updated: 2026-01-04 Status: Production-Ready Maintainer: ImproperSubset
This skill should be used when the user asks to "create an agent", "add an agent", "write a subagent", "agent frontmatter", "when to use description", "agent examples", "agent tools", "agent colors", "autonomous agent", or needs guidance on agent structure, system prompts, triggering conditions, or agent development best practices for Claude Code plugins.
This skill should be used when the user asks to "create a slash command", "add a command", "write a custom command", "define command arguments", "use command frontmatter", "organize commands", "create command with file references", "interactive command", "use AskUserQuestion in command", or needs guidance on slash command structure, YAML frontmatter fields, dynamic arguments, bash execution in commands, user interaction patterns, or command development best practices for Claude Code.
This skill should be used when the user asks to "create a hook", "add a PreToolUse/PostToolUse/Stop hook", "validate tool use", "implement prompt-based hooks", "use ${CLAUDE_PLUGIN_ROOT}", "set up event-driven automation", "block dangerous commands", or mentions hook events (PreToolUse, PostToolUse, Stop, SubagentStop, SessionStart, SessionEnd, UserPromptSubmit, PreCompact, Notification). Provides comprehensive guidance for creating and implementing Claude Code plugin hooks with focus on advanced prompt-based hooks API.