This skill should be used when choosing between flags, settings, or files for data storage, implementing document flags, registering module settings, handling file uploads, or migrating data between storage types. Covers namespacing, scope types, and performance optimization.
Chooses between flags, settings, or files for module data storage. Claude uses this when implementing document flags, registering settings, or migrating data to avoid performance issues.
/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 provides three primary storage mechanisms: Flags (document-attached), Settings (global config), and Files (external storage). Choosing the wrong method is a common source of bugs and performance issues.
| Need | Use | Why |
|---|---|---|
| Data on specific document | Flags | Travels with document, respects permissions |
| Global module config | Settings (world) | Synced to all clients, GM-controlled |
| Per-device preference | Settings (client) | localStorage, user-specific |
| Large datasets | Files | No performance impact on documents |
| Export/import data | Files | Portable, shareable |
Flags attach key-value data to Documents (Actors, Items, Scenes, etc.).
// Set a flag
await actor.setFlag('my-module', 'customProperty', { value: 42 });
// Get a flag
const data = actor.getFlag('my-module', 'customProperty');
// data === { value: 42 }
// Delete a flag
await actor.unsetFlag('my-module', 'customProperty');
// Direct access (read-only)
const value = actor.flags['my-module']?.customProperty;
Always use your module ID as the scope:
// CORRECT - Uses module ID
await doc.setFlag('my-module-id', 'flagName', value);
// WRONG - Generic scope causes collisions
await doc.setFlag('world', 'flagName', value);
// BAD - Three database writes
await actor.setFlag('myModule', 'flag1', value1);
await actor.setFlag('myModule', 'flag2', value2);
await actor.setFlag('myModule', 'flag3', value3);
// GOOD - Single database write
await actor.update({
'flags.myModule.flag1': value1,
'flags.myModule.flag2': value2,
'flags.myModule.flag3': value3
});
// Delete nested key (V10+)
await doc.unsetFlag('myModule', 'todos.completedItem');
// Alternative: Foundry deletion syntax
await doc.setFlag('myModule', 'todos', { '-=completedItem': null });
1. Periods in Object Keys Break getFlag:
// BROKEN - Period in key causes issues
await doc.setFlag('myModule', 'data', { 'some.key': 'value' });
const result = doc.getFlag('myModule', 'data');
// result !== { 'some.key': 'value' } - Data corrupted!
// WORKAROUND - Use class instance (treated as complex object)
class MyData {
constructor(data) { Object.assign(this, data); }
}
await doc.setFlag('myModule', 'data', new MyData({ 'some.key': 'value' }));
2. Inactive Module Throws Error:
// UNSAFE - Throws if module not installed
const value = doc.getFlag('optional-module', 'flag');
// SAFE - Handle missing module
const value = doc.flags['optional-module']?.flag ?? defaultValue;
Settings store global configuration with different scopes.
| Scope | Storage | Editable By | Synced | Use For |
|---|---|---|---|---|
client | localStorage | Any user | No | UI prefs, device settings |
world | Database | GM only | Yes | Module config, rules |
user (V13+) | Database | That user | Yes | Per-user cross-device |
Hooks.once('init', () => {
// Client setting - per-device
game.settings.register('myModule', 'theme', {
name: 'UI Theme',
hint: 'Select your preferred theme',
scope: 'client',
config: true,
type: String,
choices: {
light: 'Light',
dark: 'Dark'
},
default: 'light',
onChange: value => applyTheme(value)
});
// World setting - shared, GM-only
game.settings.register('myModule', 'enableFeature', {
name: 'Enable Feature',
hint: 'Turns on the special feature for all users',
scope: 'world',
config: true,
type: Boolean,
default: false,
requiresReload: true // V10+ prompts user to reload
});
});
For complex config, hide the setting and use a FormApplication:
// 1. Register menu button
game.settings.registerMenu('myModule', 'configMenu', {
name: 'Advanced Configuration',
label: 'Configure',
icon: 'fas fa-cog',
type: MyConfigApp,
restricted: true // GM only
});
// 2. Register hidden backing setting
game.settings.register('myModule', 'config', {
scope: 'world',
config: false, // Hidden from settings UI
type: Object,
default: { option1: true, threshold: 10 }
});
// 3. Access in FormApplication
class MyConfigApp extends FormApplication {
getData() {
return game.settings.get('myModule', 'config');
}
async _updateObject(event, formData) {
await game.settings.set('myModule', 'config',
foundry.utils.expandObject(formData)
);
}
}
// Choices dropdown
game.settings.register('myModule', 'mode', {
type: String,
choices: { a: 'Option A', b: 'Option B' },
default: 'a'
});
// Number with range slider
game.settings.register('myModule', 'volume', {
type: Number,
range: { min: 0, max: 100, step: 5 },
default: 50
});
// File picker
game.settings.register('myModule', 'backgroundImage', {
type: String,
filePicker: 'image', // 'audio', 'video', 'any'
default: ''
});
// DataModel for validation (recommended)
game.settings.register('myModule', 'validated', {
type: MyDataModel,
default: {}
});
// Client scope: fires only on this client
game.settings.register('myModule', 'clientSetting', {
scope: 'client',
onChange: value => {
// Only runs locally
}
});
// World scope: fires on ALL clients
game.settings.register('myModule', 'worldSetting', {
scope: 'world',
onChange: value => {
// Runs everywhere when GM changes it
// Re-fetch to ensure consistency
const current = game.settings.get('myModule', 'worldSetting');
}
});
Use file storage for large datasets or exportable data.
// Upload a file
const file = new File(
[JSON.stringify(data, null, 2)],
'export.json',
{ type: 'application/json' }
);
await FilePicker.upload(
'data', // source: 'data', 'public', 's3'
'myModule/data', // target directory
file,
{},
{ notify: true }
);
// Fetch and parse JSON
async function loadData(path) {
const response = await fetch(path);
if (!response.ok) throw new Error(`Failed to load ${path}`);
return response.json();
}
const data = await loadData('modules/myModule/data/config.json');
Store file reference in flag, load on demand:
// Store reference
await actor.setFlag('myModule', 'dataFile', 'myModule/data/actor-123.json');
// Load when needed
async function getActorData(actor) {
const path = actor.getFlag('myModule', 'dataFile');
if (!path) return null;
return loadData(path);
}
// BAD - Config doesn't belong on a random document
await game.user.setFlag('myModule', 'globalConfig', config);
// GOOD - Use world setting
game.settings.register('myModule', 'globalConfig', {
scope: 'world',
config: false,
type: Object
});
// BAD - Slows every actor update
await actor.setFlag('myModule', 'history', arrayWith10000Entries);
// GOOD - Store in file, reference in flag
const file = new File([JSON.stringify(history)], `${actor.id}-history.json`);
await FilePicker.upload('data', 'myModule/history', file);
await actor.setFlag('myModule', 'historyFile', `myModule/history/${actor.id}-history.json`);
// BAD - Each user sees different value
game.settings.register('myModule', 'gameRule', {
scope: 'client', // Wrong scope!
type: Boolean
});
// GOOD - World scope for shared rules
game.settings.register('myModule', 'gameRule', {
scope: 'world',
type: Boolean
});
Hooks.once('ready', async () => {
const version = game.settings.get('myModule', 'schemaVersion') ?? 0;
if (version < 2) {
// Collect data from actor flags
const migrated = {};
for (const actor of game.actors) {
const old = actor.getFlag('myModule', 'oldData');
if (old) {
migrated[actor.id] = old;
await actor.unsetFlag('myModule', 'oldData');
}
}
// Store in setting
await game.settings.set('myModule', 'migratedData', migrated);
await game.settings.set('myModule', 'schemaVersion', 2);
ui.notifications.info('MyModule: Migration complete');
}
});
async function migrateToFile() {
const largeData = game.settings.get('myModule', 'bigSetting');
// Export to file
const file = new File(
[JSON.stringify(largeData, null, 2)],
'migrated-data.json',
{ type: 'application/json' }
);
await FilePicker.upload('data', 'myModule', file);
// Update setting to path reference
await game.settings.set('myModule', 'dataPath', 'myModule/migrated-data.json');
await game.settings.set('myModule', 'bigSetting', null);
}
init hookscope: 'world' for shared config, scope: 'client' for preferencesdocument.update() when setting multiplerequiresReload: true for settings that need refresh (V10+)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.