Every character file in a StoryForge book project is validated by the validate_character.py PostToolUse hook on save. This page documents:
Concept β Profile β Backstory β Arc Defined β Final)The matching plugin template is at templates/character.md. This page surfaces what the template's inline comments say so you don't have to dig into the source.
| Book category | Directory | Example |
|---|---|---|
fiction |
{book}/characters/{slug}.md |
blood-and-binary-firelight/characters/theo-wilkons.md |
memoir |
{book}/people/{slug}.md |
my-memoir/people/grandfather.md |
| Series | series/{series}/characters/{tracker-slug}.md |
blood-and-binary/characters/king-caelan.md |
INDEX.md is skipped by the validator. One file per character β never combine two characters into one file (the validator does not enforce this, but the indexer expects 1-file-1-character).
The series-tracker location is documented in Series-character trackers below.
The validator requires this minimum:
---
name: "<display name>"
slug: <kebab-case-slug>
role: <role>
status: <status>
---
name, role, and status are required β missing fields cause the hook to BLOCK the save. slug is conventional but not enforced. Additional fields like species, age, height_cm, pov, title are free-form metadata; the hook ignores them.
POV characters typically also carry the snapshot fields used by pov_character_inventory and pov_character_state:
pov: true
current_inventory: []
current_clothing: []
current_injuries: []
altered_states: []
environmental_limiters: []
as_of_chapter: ""
These get populated automatically by the chapter-writer Step 7.8 write-back at chapter close.
series_evolution_imported_from)When a character file is populated via the bootstrap skill (/storyforge:bootstrap-book-from-series β see Series Lifecycle below), an extra frontmatter field records which prior band the starting state was imported from:
series_evolution_imported_from: B1
This is informational. It tells you (and the chapter-writer) where the file's snapshot values came from. You're free to edit any field afterward β the marker is not re-checked once written. It exists so a later quick scan of a book's characters/ directory can answer "which of these were hand-written vs. bootstrapped from a prior book?"
scan_for_named_characters β the resolver that populates characters_present in the chapter-writing brief β matches the name field and any aliases. This means an outline that mentions Sera correctly resolves to seraphina.md when the file carries name: Seraphina "Sera".
Two alias sources are supported and combined automatically:
Auto-extracted from name (zero config): Any quoted substring inside the name field is treated as an implicit alias. Seraphina "Sera", Theodore "Theo" Wilkons, Dominic "Dom" all work out of the box β no extra field needed as long as the nickname appears in quotes in the name value.
Explicit aliases: list: For handles that don't follow the quoted-nickname convention, add a list:
aliases: ["S.", "Sera"]
Both sources are combined and matched with word-boundary lookaround ((?<!\w)...(?!\w)) to avoid false positives β Lin does not match inside Linguistics, and dotted initials like S. are handled correctly where \b would fail.
The commented-out example is already in templates/character.md.
The role taxonomy covers the core protagonist/antagonist tiers plus the standard fiction-craft archetypal roles authors and skills reach for naturally β Hero's-Journey beats, romance arcs, mystery confidants. minor is the catch-all that also gates section-recommendation warnings (minor characters skip the recommended-sections check).
| Role | When to use |
|---|---|
protagonist |
The POV main character |
deuteragonist |
Co-equal to the protagonist β second-most-prominent character |
antagonist |
Primary opposing force |
love-interest |
The romantic anchor in romance / paranormal-romance / dark-fantasy with romance arcs |
mentor |
Hero's-Journey mentor archetype β guides the protagonist (mystery, fantasy, coming-of-age) |
foil |
Contrast character β highlights the protagonist's traits by opposition (literary fiction, thrillers) |
herald |
Hero's-Journey herald β delivers the call-to-action that kicks off the plot |
confidant |
Trusted ear for the protagonist's inner state (mysteries, thrillers, family drama) |
supporting |
Named recurring characters with their own scenes or arcs |
minor |
One-off or subplot-only characters; the validator skips section recommendations for these |
Unknown roles produce a WARN (the hook lists the valid set in the message).
Concept β Profile β Backstory β Arc Defined β Final
| Status | What it means | What the file should contain at minimum |
|---|---|---|
Concept |
Idea sketched, role known | Name, role, brief description |
Profile |
Profile fleshed out | + Appearance, Personality, basic Voice |
Backstory |
Wound and lie identified | + The Ghost, Want vs. Need, Fatal Flaw |
Arc Defined |
Arc beats mapped end-to-end | + Character Arc with explicit beats (Setup β Challenge β Midpoint β Crisis β Resolution) |
Final |
Production-ready, locked | + Voice with sample dialog, all relationships defined |
The status is informational β it does not gate any tooling. It's a signal to the author about how mature the character file is.
The hook warns when these are missing on a non-minor character. Each section has a defined purpose; the template body has inline guidance.
## Want vs. NeedThe character's external goal vs. their internal truth. Three components:
Example β Theo (blood-and-binary-firelight):
Want: To prove he's more than what people see
Need: To accept that he is enough as he is
Lie: "I'm not enough. I must prove myself to earn love."
## The Ghost (or under ## Backstory as a sub-section)The single disastrous event β or sustained condition β that changed them forever. The psychic injury they have spent their life protecting themselves from. This is the root of The Lie.
The validator accepts both placements:
## The Ghost## Backstory β ### The Ghost## Backstory / The WoundExample β Kael (blood-and-binary-firelight):
Born into royalty he never wanted. Named after his father β a constant reminder of expectations. Fifty years ago, he left. The family let him go out of love. What he doesn't know: his father had him watched the entire time.
## Fatal FlawNot just a weakness β a flaw that actively causes problems, connects to the theme, and has roots in The Ghost. It must be overcome (positive arc) or embraced (negative arc).
Example β Theo:
Self-deprecating self-doubt that he projects onto his relationships. He doesn't just doubt himself β he doubts that anyone could genuinely want him. This pushes people away and creates self-fulfilling prophecies.
Example β Kael:
Overprotection born from fear of loss. What starts as love becomes control. He becomes the authority figure he fled from.
## Motivation Chain (or ## GMC)Dig beneath the surface. Ask "why?" three times.
The validator accepts these aliases:
## Motivation Chain (template-canonical)## GMC## GMC (Goal / Motivation / Conflict)## Character ArcArc type (Positive / Negative / Flat) plus the beats:
Required for Arc Defined status; recommended for Final.
## RelationshipsOptional but strongly recommended. Used by the harvest skill (Series Lifecycle) to surface cross-character dynamics back into the series tracker at end-of-book. Format as a bulleted list with bold names:
## Relationships
- **Theo:** Lover and partner β the relationship that pulls Kael back into the family fold.
- **Caelan:** Father β Kael's longest-running unresolved conflict.
Not every named character needs Arc Defined. The validator's recommendations are tier-aware:
| Character type | Typical status | Sections expected |
|---|---|---|
| Protagonist (POV) | Arc Defined or Final |
All recommended + Voice with sample dialog |
| Deuteragonist | Arc Defined |
All recommended (their arc is co-equal to the protagonist's) |
| Antagonist | Backstory minimum, Arc Defined if they have an on-page arc |
Want vs. Need, Fatal Flaw, The Ghost β even villains have an internal logic |
| Love-interest | Backstory minimum in romance-driven books, Arc Defined if their arc is co-equal |
Want vs. Need + Ghost; full arc when the relationship arc is the engine of the book |
| Mentor / Herald / Foil / Confidant | Profile to Backstory |
Voice + the slice of psychology that powers their function β full arc usually optional |
| Supporting with own arc | Backstory |
Ghost + Arc, sometimes Want/Need; Fatal Flaw optional |
| Supporting (recurring) | Profile |
Personality, Voice, basic Appearance β full psychology not required |
| Minor (subplot only) | Profile or Concept |
Just enough to render them on the page |
Example from blood-and-binary-firelight:
| Character | Role | Status | Why |
|---|---|---|---|
| Theo Wilkons | protagonist | Arc Defined | Full psychology + arc beats, POV |
| Kael | deuteragonist | Arc Defined | Full psychology + arc beats |
| Jace Reeves | supporting | Backstory | Has a Ghost (lost mother) + emerging arc, but full Want/Need not yet pinned |
| Lucien | antagonist | Profile | Plot-mechanical antagonist; Backstory pending |
| Viktor / Dominic / Sera / Miriel / Caelan | supporting | Profile | Personality + Voice locked, no individual psychological deep-dive needed |
| Kevin | minor | Profile | Subplot bully, last appearance Ch 10; sections skipped |
The validate_character.py PostToolUse hook is registered in .claude-plugin/hooks.json and runs on every Write / Edit / MultiEdit operation that touches a *.md file in a characters/ or people/ directory.
The write is rejected. The hook prints a message to stderr that Claude Code shows the model:
| Condition | Example message |
|---|---|
| No YAML frontmatter | theo.md β Missing YAML frontmatter |
| Invalid YAML | theo.md β Invalid YAML frontmatter: <parser error> |
| Missing required field | theo.md β Missing frontmatter field: 'status' |
| Frontmatter is not a YAML mapping | theo.md β Frontmatter is not a YAML mapping |
The write succeeds; the hook prints a [WARN] line to stdout for visibility:
| Condition | Example message |
|---|---|
| Unknown role | theo.md β Unknown role 'wizard'. Valid: antagonist, confidant, deuteragonist, foil, herald, love-interest, mentor, minor, protagonist, supporting |
| Missing recommended section | theo.md β Missing recommended section '## Want vs. Need' |
/characters/ or /people/ pathsINDEX.md filesperson_category, consent_status, or real_name, the role check and section recommendations are skipped (memoir uses a different schema β see memoir-ethics-checker)series/{slug}/characters/ (different schema β see Series-character trackers)The matcher accepts headers at level 2 (## Section) or level 3 (### Section). Section title alternatives are also accepted β for example, the The Ghost recommendation is satisfied by any of:
## The Ghost## Backstory containing ### The Ghost## Backstory / The Wound (legacy alias)## Backstory (any depth, treated as containing the Ghost material)This handles both modern templates (where The Ghost is a level-3 sub-section under ## Backstory) and legacy character files that use level-2 placement directly.
Series projects have an additional character-file location: series/{series}/characters/{tracker-slug}.md. These are series-evolution trackers β thin essence files that capture what's constant about a character across all books in the series, plus per-book Evolution sections.
| File type | Path | Purpose |
|---|---|---|
| Book character file | projects/{book}/characters/{slug}.md |
Full profile β psychology, arc, voice, sample dialog. Source of truth for one book. |
| Series tracker | series/{series}/characters/{tracker-slug}.md |
Cross-book through-line β Snapshot, Evolution per Band, cross-book relationships. |
The two are linked. The series tracker carries an optional book_slug: frontmatter field that points to the matching book-level file:
---
name: "King Caelan"
slug: king-caelan # the tracker-level slug
book_slug: caelan # optional: the book-level file is caelan.md
role: protagonist
species: vampire
status: "Profile"
recurs_in: ["B1", "B2"]
tracker_type: "thin"
---
When book_slug: is absent, the resolver (resolve_book_slug_for_series_tracker()) falls back to slug: β the zero-config default for the common case where the tracker slug and the book-level slug already match (kael β kael.md).
Set the field only when the series-planner picked a role/title-prefixed tracker slug for a character whose book-level file uses the bare name. In blood-and-binary this happened for three of thirteen trackers:
| Tracker file | slug: |
book_slug: |
Book-level file |
|---|---|---|---|
king-caelan.md |
king-caelan |
caelan |
caelan.md |
queen-miriel.md |
queen-miriel |
miriel |
miriel.md |
lord-lucien.md |
lord-lucien |
lucien |
lucien.md |
The tracker_type field has two values:
thin (default) β the tracker is a series-scope projection; the book-level file is the source of truth for full profile, voice, and arcfull β the tracker IS the source of truth (rare; only when a character spans books equally without a "home" book)A tracker file has four canonical sections:
## Snapshot
One-paragraph essence at series scope. What is constant about this character
across all books? Identity, role, key relationship anchors.
## Evolution per Band
### B1 Firelight
- **Start:** Where the character begins in B1.
- **Ende:** Where the character ends in B1.
### B2 Moonrise (geplant)
- Plan bullets for B2.
## Beziehungen ΓΌber die BΓ€nde
- **Other Character:** B1 dynamic β B2 shift β B3 resolution.
## Updates Log
- 2026-05-08 β Tracker scaffolded by series-planner
- 2026-06-12 β Harvested from B1 final state
The matching template is at templates/series-character-tracker.md. The parser is tolerant of two shapes β bullet-keyed (### B1 Firelight with **Start:** / **Ende:** bullets) and separate-H3 (### B1 Start, ### B1 Ende) β and writers preserve whichever shape your tracker already uses.
How a character moves from one book to the next across a series. This is the workflow that keeps multi-book casts coherent without manual copy-paste between book and tracker files.
B1 Drafting
β
chapter-writer Step 7.8 writes pov_character_inventory + pov_character_state
back to characters/{slug}.md frontmatter at each chapter close
β
B1 reaches Final
β
/storyforge:harvest-character-evolution firelight
β Reads each tracker's recurs_in field
β Reads each book character's end-of-book state (snapshot + Relationships section)
β Synthesizes a B1 Ende summary per character
β Walks char-by-char with you (Accept / Edit / Skip / Keep existing)
β Writes confirmed Ende text to the tracker, appends an Updates Log entry
β
Tracker now holds the canonical end-of-B1 state for every recurring character
β
/storyforge:new-book moonrise --series=blood-and-binary --copy-recurring-from=blood-and-binary-firelight
β Auto-copies all recurring char files (filtered by recurs_in containing B2)
β Excludes B1-only chars (e.g. characters who die in B1)
β Flags B2-first-appearance chars for manual creation
β
moonrise/characters/ now contains 1:1 copies of the B1 char files
β
/storyforge:bootstrap-book-from-series firelight moonrise
β Reads each tracker's B1 Ende (what harvest wrote)
β Reads each tracker's B2 (geplant) section (what series-planner wrote at planning time)
β Synthesizes a B2-start snapshot per character (LLM judgment)
β Walks char-by-char with you (Accept / Edit / Skip)
β Writes confirmed snapshot to moonrise/characters/{slug}.md frontmatter
β Adds series_evolution_imported_from: B1 marker
β Appends an Updates Log entry on the tracker
β
moonrise character files now reflect B2-start state, ready for chapter-writer
β
chapter-writer for moonrise reads characters_present[*].series_evolution
β previous_book_end: where the character left off in B1
β current_book_plan: what's planned for B2
β relationships_evolution: cross-book relationship arcs
β Writer drafts in voice without manual lookup
β
Loop back to chapter-writer Step 7.8 β B2 drafting begins, snapshots get
written back to character files at chapter close. When B2 reaches Final,
harvest runs again with prev_band=B2, and B3 bootstrapping repeats the loop.
| Skill / tool | When | Reads | Writes |
|---|---|---|---|
chapter-writer (Step 7.8) |
every chapter close, in any book | Outline + draft | characters/{slug}.md frontmatter snapshot fields |
/storyforge:harvest-character-evolution |
end-of-book, after Final status |
Book character snapshot + Relationships | Tracker ### B{N} Ende + Updates Log |
/storyforge:new-book ... --copy-recurring-from=... |
start-of-next-book | Tracker recurs_in filter + prior book character files |
New book characters/ (1:1 copies) |
/storyforge:bootstrap-book-from-series |
after new-book scaffolds, before drafting begins | Tracker B{prev} Ende + B{new} (geplant) |
New book character file frontmatter snapshot + series_evolution_imported_from marker + Tracker Updates Log |
chapter_writing_brief (series_evolution field) |
every chapter brief in a series book | Tracker B{prev} Ende, B{current} (geplant), Beziehungen |
Read-only β surfaced to chapter-writer |
All series-evolution behavior degrades gracefully. Standalone novels don't carry a series: field in their README, so:
harvest-character-evolution reports "Book is not part of a series β nothing to harvest" and exits cleanlybootstrap-book-from-series requires both books to share a series and errors out otherwisechapter_writing_brief sets series_evolution: null on every character β the field is always present so downstream consumers can rely on it, but the value is None for non-series booksThe same null result happens when:
recurs_in does not include the current bandThis means you can run any of these skills against any book β they just won't do anything in books where the data isn't there, and they won't break the brief for chapters that mix series characters with one-off book-only characters.
The reverse-lookup that powers the brief enrichment uses the same book_slug: resolver as the harvest and bootstrap skills. If your tracker file is king-caelan.md and the book character file is caelan.md, set book_slug: caelan in the tracker frontmatter. Without it, the brief returns series_evolution: null for that character β graceful degrade, not an error. Adding the field is a one-line edit and the bridge starts working immediately.
The repo at ~/projekte/book-projects/projects/blood-and-binary-firelight/characters/ contains 10 files validated by the modernized hook:
theo-wilkons.md β protagonist, Arc Defined, 0 warnings (full template adherence)kael.md β deuteragonist, Arc Defined, 0 warningsjace-reeves.md β supporting, Backstory, 3 warnings (intentional β Want/Need still emerging)caelan.md, miriel.md, dominic.md, viktor.md, seraphina.md β supporting, Profile, 4 warnings each (intentional β Profile tier doesn't require full psychology)lucien.md β antagonist, Profile, 3 warnings (Backstory upgrade is on the roadmap)kevin.md β minor, Profile, 0 warnings (minor role skips section recommendations)The warnings are informational. None of them block the save. The validator's job is to surface where each character file currently sits on the maturity curve, not to enforce a one-size-fits-all template.