Selena cast Mage Armor and jumped off a three-story building. Mage Armor does not protect against gravity. She took 15 bludgeoning damage, went to 0 HP, and spent the next several minutes unconscious on a noodle cart while a frog-man attempted emergency medicine on her, and rolled a natural 1.
That was Session 1. It ran.
What it is
A Claude Code skill (~/.claude/skills/dnd-dm/SKILL.md + 305 JSON data files) that turns Claude into a full Dungeon Master for a Japanese folklore-inspired D&D 5e setting. Built as a side project. Somehow has 305 JSON files. It handles rules lookup, campaign state, NPC behaviour, dice resolution, voice narration, and session persistence. No app, no server, no build step. Just a skill file Claude reads at the start of the conversation.
The setting: one island, spirits everywhere (they're your neighbours, your shopkeepers, your pests), First Age technology (cassette players, vending machines, bicycles) powered by spirit energy, and a slow-moving Corruption spreading from the east. Warm and cozy until it isn't.
Campaign state machine
Everything that matters lives in three JSON files:
// campaign.json: current scene, world state, quest log
{
"current_scene": {
"location": "yatamon-fire-snake-alley",
"region": "Gift of Shuritashi",
"time": "night",
"description": "Selena unconscious on a commandeered noodle cart. Pip talked down city guards (23 vs DC 14). Party needs to get out of Yatamon.",
"threats": ["city-guard-patrol, guards wrote something down, faces are known"]
},
"world_flags": {
"cops_pursuing": true,
"grandmother_mystery": "letter received, told to find Bim of the Beasts at the Corrupted Coastline",
"magic_hat_found": true
}
}
Players make a decision. The DM narrates. State updates. The session log gets an entry. Repeat until they flee into a First Age subway tunnel for a short rest.
Character sheets are their own JSON files (players/selena.json), updated after every significant event: spell slots consumed, items found, conditions applied. Selena's sheet tracks hp.current, spell_slots, conditions, equipment, and her custom traits:
"custom_traits": {
"chaotic_luck": "Once per session DM secretly rolls d6: 1-2 = next roll goes badly, 5-6 = goes spectacularly, 3-4 = normal. Player never knows when it triggers.",
"excessive_packer": "Speed permanently 20 ft. Double carrying capacity. Magic Hat negates speed penalty when worn.",
"clumsy": "DEX 9. Disadvantage on Acrobatics. On critical DEX failures, something breaks or spills.",
"good_in_a_crisis": "At or below 25% HP or in dire party situations: advantage on all ability checks."
}
Free-form personality traits mapped to actual mechanics. The DM reads these at session start and runs them quietly; players discover the consequences, not the rules.
Pre-calculated outcomes
The core speed trick. Before asking the player to roll, the DM pre-calculates all four possible outcomes and writes them as one-liners. The player rolls, the DM delivers the matching line instantly.
Selena jumping off the third-floor ledge looked like this:
DEX save DC 12: the awning is right there, you just need to clip it.
- Nat 1: Full 30-foot drop onto cobblestones. [3d6: roll now]
- 2-11: You miss the awning and hit the grass mat cart below. [2d6]
- 12-19: You catch the awning edge, swing down, land clean.
- Nat 20: Wizard parkour. Pip is impressed.
She rolled a 5. Three dice of fall damage, unconscious, death saves. No pause, no lookup. The outcome was already written.
The Bash call happens before the player rolls:
# Pre-roll all damage tiers in one call
FULL=$(jot -r 3 1 6 | paste -sd+ - | bc)
HALF=$(jot -r 2 1 6 | paste -sd+ - | bc)
echo "Full:$FULL Half:$HALF"
Monster turns batch the same way: all attacks and damage in one shell call, then the DM narrates the whole block.
The fail-forward principle
A dead wizard in session one is bad storytelling. The fail-forward principle: failed rolls create complications, not dead ends.
Selena hit 0 HP with 8 max HP and took 15 damage. Excess of 7, under max HP, so no instant death. Death saves active. Three successes needed, three failures = dead.
Pip attempted Medicine (DC 10 to stabilise). Natural 1. He pressed directly on her bruised ribs. She took 1 additional damage. The session log captured it:
Pip botched first Medicine check (nat 1); caused 1 damage to Selena. Sebastian blocked further interference. Pip found vendor napkins, re-attempted with advantage, rolled 18; Selena stabilised.
Sebastian (Selena's companion spirit, a black cat who speaks only in the third person) physically blocked Pip from making things worse. "Sebastian has concerns." That interaction wasn't scripted. It came from the companion spirit's personality (Disposition: Brave, Quirk: Third Person) colliding with the mechanical reality of a character bleeding out.
The data files
305 JSON files covering the setting: 76 monsters, 50 spells, 48 items, 43 NPCs, 27 locations, 17 lore entries, 11 custom subclasses, 8 factions, 7 regions. Three full pre-written adventures.
Every mechanical ruling comes from a file or the 5e SRD API. The skill file's Rule 0: never improvise numbers. Narrative creativity is encouraged. Mechanical improvisation is banned.
When a player casts a spell the DM hasn't run before:
curl -sL "https://www.dnd5eapi.co/api/spells/mage-armor"
If the monster isn't in the local files:
curl -sL "https://www.dnd5eapi.co/api/monsters/goblin"
If the API is down, the DM asks the players for the numbers. No guessing.
Voice narration
When ELEVENLABS_API_KEY is set, scene descriptions get piped to a voice script; background process, so the game doesn't wait:
echo "The ancient door groans open, and warm air spills out from below, the smell of stone and something electric, like a storm about to break." | narrate.sh &
Atmospheric prose only. Dice results, HP changes, and rules text stay silent. The & is mandatory. Blocking on audio would kill pacing.
Two voices: George (warm British storyteller, default) and Sarah (mature, intimate). Players choose at session start.
Where Session 1 ended
Pip talked down two city guards with a 19+4=23 Deception check. His argument: "These are not the criminals you are looking for. She fell. Off a balcony. I'm her physician. The mushrooms are medicinal. The cat is licensed." Squib (his bat companion) hung upside down from his ear and stared at the guards throughout. The taller guard squinted for a long time. Then walked away. The shorter one wrote something in a notebook.
Sebastian, watching: "Sebastian did not expect that to work."
Sebastian led them to a First Age subway tunnel three alleys from the shop. Warm, dry, bioluminescent Dustbunny spirit on the ceiling. Selena on the noodle cart. Short rest in progress.
The session state is saved. The quest log has four active entries: get out of Yatamon, find Bim of the Beasts, learn the truth about grandmother's death, get rich. Selena wakes up next session and rolls 1d6+2.