This skill should be used when implementing multiplayer synchronization, using game.socket.emit/on, creating executeAsGM patterns for privileged operations, broadcasting events between clients, or avoiding common pitfalls like race conditions and duplicate execution.
Implements multiplayer-safe Foundry VTT operations using sockets for broadcasting events and socketlib for GM-delegated privileged actions. Use when synchronizing state across clients, executing document updates requiring GM permissions, or avoiding race conditions in multiplayer environments.
/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 Socket.io for real-time communication between server and clients. Understanding socket patterns is essential for multiplayer-safe code.
Request socket access in your manifest:
{
"id": "my-module",
"socket": true
}
Each package gets ONE event namespace:
module.{module-id}system.{system-id}Multiplex event types with structured data:
const SOCKET_NAME = "module.my-module";
game.socket.emit(SOCKET_NAME, {
type: "playAnimation",
payload: { tokenId: "abc123", effect: "fire" }
});
Register listeners after game.socket is available:
Hooks.once("init", () => {
game.socket.on("module.my-module", handleSocketMessage);
});
function handleSocketMessage(data) {
switch (data.type) {
case "playAnimation":
playTokenAnimation(data.payload);
break;
case "syncState":
updateLocalState(data.payload);
break;
}
}
function broadcastAnimation(tokenId, effect) {
game.socket.emit("module.my-module", {
type: "playAnimation",
tokenId,
effect
});
}
Critical: Emitting client does NOT receive its own broadcast.
Always call handler locally when emitting:
function triggerEffect(tokenId, effect) {
const data = { type: "effect", tokenId, effect };
// Execute locally
handleEffect(data);
// Broadcast to others
game.socket.emit("module.my-module", data);
}
function handleEffect(data) {
const token = canvas.tokens.get(data.tokenId);
token?.animate({ alpha: 0.5 }, { duration: 500 });
}
// Socket listener (for other clients)
Hooks.once("init", () => {
game.socket.on("module.my-module", (data) => {
if (data.type === "effect") handleEffect(data);
});
});
Players often need GM-authorized operations (damage enemies, modify world data).
const SOCKET_NAME = "module.my-module";
Hooks.once("init", () => {
game.socket.on(SOCKET_NAME, async (data) => {
// Only active GM handles this
if (game.user !== game.users.activeGM) return;
if (data.type === "damageActor") {
const actor = game.actors.get(data.actorId);
if (actor) {
const newHp = actor.system.hp.value - data.damage;
await actor.update({ "system.hp.value": Math.max(0, newHp) });
}
}
});
});
// Player calls this
function requestDamage(actorId, damage) {
game.socket.emit(SOCKET_NAME, {
type: "damageActor",
actorId,
damage
});
}
Limitations:
Socketlib handles multiple GMs, return values, and error cases.
Dependency (module.json):
{
"relationships": {
"requires": [{
"id": "socketlib",
"type": "module"
}]
}
}
Registration:
let socket;
Hooks.once("socketlib.ready", () => {
socket = socketlib.registerModule("my-module");
// Register callable functions
socket.register("damageActor", damageActor);
socket.register("getActorData", getActorData);
});
async function damageActor(actorId, damage) {
const actor = game.actors.get(actorId);
if (!actor) return { success: false, error: "Actor not found" };
const newHp = Math.max(0, actor.system.hp.value - damage);
await actor.update({ "system.hp.value": newHp });
return { success: true, newHp };
}
function getActorData(actorId) {
return game.actors.get(actorId)?.toObject() ?? null;
}
Usage:
// Execute on GM client, get return value
async function applyDamage(actorId, damage) {
try {
const result = await socket.executeAsGM("damageActor", actorId, damage);
if (result.success) {
ui.notifications.info(`Damage applied. HP now: ${result.newHp}`);
}
} catch (error) {
ui.notifications.error("No GM connected to process damage");
}
}
| Method | Target | Awaitable | Use Case |
|---|---|---|---|
executeAsGM(fn, ...args) | One GM | Yes | Privileged operations |
executeAsUser(fn, userId, ...args) | Specific user | Yes | Player-specific actions |
executeForEveryone(fn, ...args) | All clients | No | Broadcast effects |
executeForOthers(fn, ...args) | All except self | No | Sync without local call |
executeForAllGMs(fn, ...args) | All GMs | No | GM notifications |
executeForUsers(fn, ids[], ...args) | Listed users | No | Targeted messages |
// Trigger animation on ALL clients
function playGlobalEffect(effectData) {
socket.executeForEveryone("renderEffect", effectData);
}
// Registered function
function renderEffect(data) {
canvas.effects.playEffect(data);
}
// Ask specific player for input
async function promptPlayer(userId, question) {
try {
return await socket.executeAsUser("showDialog", userId, question);
} catch {
return null; // Player disconnected
}
}
// Registered function
async function showDialog(question) {
return new Promise(resolve => {
new Dialog({
title: question,
buttons: {
yes: { label: "Yes", callback: () => resolve(true) },
no: { label: "No", callback: () => resolve(false) }
}
}).render(true);
});
}
Foundry syncs document updates automatically:
// Syncs to all clients
await actor.update({ "system.hp.value": 50 });
// Does NOT sync (in-memory only)
actor.system.hp.value = 50;
Use sockets for custom state:
let combatState = {};
Hooks.once("socketlib.ready", () => {
socket.register("syncCombatState", (state) => {
combatState = state;
Hooks.callAll("combatStateChanged", state);
});
});
function updateCombatState(newState) {
combatState = newState;
socket.executeForEveryone("syncCombatState", newState);
}
Only owners can update documents:
// Player cannot update enemy
await enemyActor.update({ ... }); // Permission denied!
// Must delegate to GM
await socket.executeAsGM("updateEnemy", enemyId, changes);
// WRONG - emitter never sees this
game.socket.on("module.my-module", playSound);
game.socket.emit("module.my-module", { sound: "bell.wav" });
// Sound plays for others, NOT for emitter!
// CORRECT - call locally AND emit
playSound({ sound: "bell.wav" });
game.socket.emit("module.my-module", { sound: "bell.wav" });
// WRONG - runs on ALL clients
Hooks.on("deleteItem", (item) => {
item.parent.update({ "system.count": item.parent.items.length });
});
// CORRECT - only owner executes
Hooks.on("deleteItem", (item) => {
if (!item.parent?.isOwner) return;
item.parent.update({ "system.count": item.parent.items.length });
});
// RISKY - activeGM can change during async
game.socket.on(name, async (data) => {
if (game.user !== game.users.activeGM) return;
await actor.update({ ... }); // Another GM might be active now!
});
// SAFE - socketlib guarantees atomic execution
await socket.executeAsGM("updateActor", actorId, data);
// VULNERABLE - any player can trigger
game.socket.on(name, (data) => {
game.actors.get(data.id).update({ "system.hp": 9999 });
});
// SAFE - validate permissions
game.socket.on(name, (data) => {
const actor = game.actors.get(data.id);
if (!actor?.isOwner && !game.user.isGM) return;
actor.update({ "system.hp": data.hp });
});
// WRONG - silent failure
socket.executeAsGM("doThing", data);
// CORRECT - handle error
try {
await socket.executeAsGM("doThing", data);
} catch {
ui.notifications.warn("A GM must be connected for this action");
}
// WRONG - N clients = N updates
Hooks.on("updateActor", (actor, changes) => {
actor.update({ "system.modified": Date.now() });
});
// CORRECT - only owner updates
Hooks.on("updateActor", (actor, changes) => {
if (!actor.isOwner) return;
if (changes.system?.modified) return; // Prevent loop
actor.update({ "system.modified": Date.now() });
});
// Good - clear, maintainable
game.socket.emit(SOCKET_NAME, {
type: "applyEffect",
targetId: token.id,
effectType: "fire",
duration: 3000
});
// Bad - 3 updates
await actor.update({ "system.hp": 10 });
await actor.update({ "system.mp": 5 });
await actor.update({ "system.status": "hurt" });
// Good - 1 update
await actor.update({
"system.hp": 10,
"system.mp": 5,
"system.status": "hurt"
});
const newHp = calculateHp(actor);
if (actor.system.hp.value === newHp) return;
await actor.update({ "system.hp.value": newHp });
/**
* Socket: module.my-module
*
* @event applyDamage
* @param {string} actorId - Target actor
* @param {number} damage - Damage amount
* @param {string} type - Damage type (fire, cold, etc.)
*/
Native sockets for simple broadcasts. Socketlib when you need:
"socket": true to manifestmodule.X or system.X)init hooktype fieldLast 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.