Standalone books live under {content_root}/projects/{book-slug}/, volumes of a series under {content_root}/series/{series-slug}/{book-slug}/.
{book-slug}/
βββ README.md # Book metadata (YAML frontmatter)
βββ CLAUDE.md # Book rules, callbacks, linter_mode
βββ synopsis.md # Blurb and long synopsis
βββ world-rules.md # fiction only: canonically fragile facts
βββ plot/ # outline.md, acts.md, arcs.md, timeline.md, tone.md, canon-log.md
βββ characters/ # INDEX.md and one file per character
βββ world/ # setting.md (travel matrix), rules.md, history.md, glossary.md
βββ research/ # sources.md and notes/
βββ chapters/
β βββ {NN-slug}/
β βββ README.md # Chapter metadata, chapter timeline, promises
β βββ draft.md # the prose
βββ cover/ # brief.md, prompts.md, art/
βββ promo/ # created on demand
βββ export/ # front-matter.md, back-matter.md, blurb.md, output/
βββ translations/ # {language}/ with glossary.md and chapters/
With book_category: memoir, people/ replaces characters/, world/ and world-rules.md are omitted, and plot/ contains structure.md and people-log.md instead of acts.md, arcs.md and canon-log.md.
| Level | Order |
|---|---|
| Book | Idea, Concept, Research, Plot Outlined, Characters Created, World Built, Drafting, Revision, Editing, Proofread, Export Ready, Published |
| Chapter | Outline, Draft, Revision, Polished, Final |
| Character | Concept, Profile, Backstory, Arc Defined, Final |
| Idea | raw, explored, developed, ready, shelved, promoted |
The book status is derived from the chapters and only ever moves forward:
| Book status | Trigger |
|---|---|
Drafting |
at least one chapter is past Outline |
Revision |
every chapter is at least at Revision |
Proofread |
every chapter is Final |
You set Editing, Export Ready and Published yourself. A higher status you set is never downgraded. rebuild_state() writes the derived status back to the README.
Chapter status aliases are recognised: review and reviewed count as Revision, drafting as Draft, polishing as Polished, done as Final.
The plugin registers five hook commands on three events. They run through the wrapper bin/run-server with the virtual environment's Python.
| Event | Script | Effect |
|---|---|---|
PostToolUse (Write, Edit, MultiEdit) |
hooks/validate_chapter.py |
Prose check of a draft.md |
PostToolUse (Write, Edit, MultiEdit) |
hooks/validate_character.py |
Structure check of character and people files |
PostToolUse (Write, Edit, MultiEdit) |
hooks/snapshot_draft.py post |
Merges Claude's change into the comparison baseline |
PreToolUse (Write, Edit, MultiEdit) |
hooks/snapshot_draft.py pre |
Captures the draft right before Claude's write |
| PreCompact | hooks/precompact_sync_claudemd.py |
Saves rules, workflows and callbacks before context compaction |
validate_chapter)The hook runs after the write. It only checks files named draft.md below a chapters/ directory, and only when the text has at least 50 words. It locates the book root (the directory containing chapters/ and README.md) and runs these checks:
| Check | Level | Subject |
|---|---|---|
| Book rules | block | Patterns from the book rules and the banned patterns in plot/tone.md that are written in backticks |
| Author banlist | block or warn | the author's banned phrases (don'ts) and recurring tics, each with its own level |
| Meta-narrative | block | narration that talks about the book's structure, such as Ch 5, callback, as established, echoes the earlier, foreshadow, calls back to, parallels the earlier |
| Story-time anchor | warn | relative time phrases such as yesterday or an hour ago against the chapter start |
| AI tells and global shapes | warn | patterns from reference/craft/anti-ai-patterns.md (English-language books only) |
| Sentence-length variance | warn | standard deviation of sentence length below 4 words with more than 10 sentences |
| POV boundary | warn | specialist vocabulary the POV character does not know according to their knowledge profile |
In strict mode the hook exits with code 2 on a block-level finding. The message goes back to Claude, which has to correct the text. In warn mode (see configuration) blockers are only printed. Warnings never block.
StoryForge linter flagged this write as blocking:
[BLOCK] draft.md line 12: Banned phrase from book CLAUDE.md: 'clocked'
Plus 2 non-blocking warnings:
[WARN] draft.md line 8: phrase 'yesterday' implies Mon Dec 23 ~19:30
(chapter starts Tue Dec 24 ~19:30). Verify against plot/timeline.md.
The file was already written but is invalid β fix the blocking issues. Set `linter_mode: warn` in the book's CLAUDE.md frontmatter to override.
The same rule set is available as the tool validate_chapter.
validate_character)The hook acts on .md files under characters/ and people/. INDEX.md is skipped. Also skipped is only the exact path of a series tracker, series/{series}/characters/{file} or series/{series}/people/{file}. Character files of books under series/{series}/{book}/characters/ sit one level deeper and are checked normally.
name and status; for characters (not memoir person files) also role.protagonist, deuteragonist, antagonist, love-interest, mentor, foil, herald, confidant, supporting, minor).Want vs. Need, Fatal Flaw, The Ghost (or Backstory) and Motivation Chain (or GMC). Characters with role minor are exempt.person_category, consent_status or real_name) are checked neither for role nor for the recommended sections.Before every context compaction the hook reads the transcript, looks in your messages for lines with the prefixes Regel: (also Rule:), Workflow: and Callback: and stores them for the active book (the book from session.last_book in the state cache). No language model is involved, it is plain pattern matching.
CLAUDE.md, the hook does nothing.hooks/snapshot_draft.py keeps two versions of a fiction chapter. It checks nothing, never blocks, prints nothing and swallows errors. Memoir chapters are skipped.
~/.storyforge/snapshots/{book-slug}/{chapter-slug}.md (the directory is overridable via the STORYFORGE_SNAPSHOT_DIR environment variable). pre captures the draft right before Claude's write; post merges only Claude's change into the baseline through a paragraph-level three-way merge β so your own edits stay visible as the difference between draft.md and the baseline. get_self_edit_hunks reads this on request; refresh_draft_baseline resets the baseline.~/.storyforge/snapshots/.raw/{book-slug}/{chapter-slug}.md. create_chapter seeds it with the chapter heading (fiction only), after which it only grows through the chapter writer's plain appends. Writes that change existing text (reviewer, humanizer, proofreader, fixer) do not touch it. Readable with list_raw_drafts(book_slug) and read_raw_draft(book_slug, chapter_slug).A book's rules, workflows and callbacks live in the database and are created through append_book_rule, append_book_workflow, append_book_callback or the skill /storyforge:register-callback.
For the prose hook only what a rule puts in backticks counts:
| Notation | Effect |
|---|---|
`clocked` |
Hard pattern. If the text contains regex characters it is treated as a regular expression, otherwise as plain text. An exception applies to a nested, unboundedly-quantified group such as (a+)+ (ReDoS risk): it is enforced as plain text instead. Simply quantified groups such as (foo)+ or (?:being){2} remain regex. Case is ignored. |
`journey` max 2 per chapter |
Allows up to N hits per chapter. The limit is scaled by the current draft length relative to the chapter's target word count. |
Recognised limit notations: max N per chapter, maximum N per chapter, limit to N per chapter, a range max N-M per chapter (the upper bound counts) and per kapitel.
Use lint_book_rules or /storyforge:rules-audit to check rules for wordings the scanner cannot catch.
The "Banned Prose Patterns" section in a book's plot/tone.md is read by the machinery. The heading may sit at any level from ## to ######; the section runs to the next heading of the same or a shallower level. Bullets with a phrase in backticks are enforceable, and an addition such as max 1 per chapter caps hits per chapter exactly like book rules:
## Banned Prose Patterns
- `suddenly` Max 1 per chapter
- Avoid too many rhetorical questions in a row.
In the example only the first bullet is enforceable. The second carries no backtick phrase and stays a plain hint that nothing scans: reviewers see it under tonal_rules.banned_prose_unenforced, the humanizer warns about it, and the chapter writing brief carries it under tone_banned_patterns.advisory_unenforced.
Enforceable tone rules act like book rules: the prose hook blocks them with a per-chapter cap, the manuscript checker reads them in alongside book rules, and scan_chapter_ai_tells/check_replacement_text honor them. The template templates/plot-tone.md sets the heading at level 2.
The global catalog (reference/craft/anti-ai-patterns.md, section 1 vocabulary and section 11 banned shapes) applies only to books written in English. The language comes from the language field in the book's README.md YAML frontmatter (default en; en-US counts as en). For any other language a file anti-ai-patterns.<language>.md is expected; none exists at present, so the catalog hits are simply dropped for such books rather than applying English patterns blindly. This affects the hook categories ai_tell and global_shape_violation, the manuscript checker's ai_tell_violation and global_shape_violation, and the humanizer's pre-scan. scan_chapter_ai_tells and check_replacement_text return a warning instead of catalog hits for such books. Book rules, author bans, tics and tone rules still apply regardless of the book's language.
The author's discoveries (banned phrases, recurring tics, don'ts, style principles) live in the database authors.db. Of these, the prose hook only scans the don'ts and the recurring tics. It does not scan style principles. chapter-writer, chapter-reviewer and author-check read them through get_author and the chapter writing brief. The file vocabulary.md is reference only and is not read for enforcement. Add a new banned phrase with write_author_banned_phrase or add_vocabulary_entry. create_author also seeds 19 common AI-tell words (such as delve, tapestry, nuanced, resonate, pivotal) into the database as don'ts on creation, and they take effect immediately.
The hook finds a book's author through the line - **Author:** Name in the Book Facts section of the CLAUDE.md, falling back to the author: field in the YAML frontmatter of the book's README.md.
Each chapter can have a ## Chapter Timeline section in its README.md with start and end times of the scenes, for example **Start:** Tue Dec 24 ~19:30. From the start the plugin computes which date relative phrases such as yesterday or tomorrow mean. The prose hook warns on every such phrase with the derived date so you check it against plot/timeline.md. The tool get_current_story_anchor returns the mapping directly:
{
"available_relative_phrases": {
"yesterday": "Mon Dec 23 ~19:30",
"tomorrow": "Wed Dec 25 ~19:30"
}
}
get_recent_chapter_timelines(book_slug, n) returns the time grids of the last N chapters so that new chapters are anchored to real times instead of remembered ones. validate_timeline_consistency checks time drift across the whole book.
get_chapter_writing_brief(book_slug, chapter_slug) replaces many single read steps in the skill chapter-writer with one JSON containing these parts:
chapter, pov_characterstory_anchor, recent_chapter_timelines, recent_chapter_endingscharacters_present with profile, knowledge, tactical and series_evolution (always present, null without series context; filled for series volumes with tracker text, resolved for both layouts projects/{book} and series/{series}/{book})pov_character_inventory (the POV character's last established possessions)rules_to_honor, callbacks_in_register, banned_phrasesrecent_simile_count_per_chapter, tone_litmus_questions, tone_banned_patterns (enforced, advisory_unenforced)tactical_constraints (only for combat or travel scenes), review_handleerrorsEach component is guarded individually. If one fails, it appears in errors and the brief still arrives with the remaining data. A frequent cause is a missing world/setting.md or plot/timeline.md.
verify_tactical_setup checks combat and travel scenes against the optional tactical block in a character's frontmatter:
tactical:
protector_role: false
protected_role: true
combat_skill: none # none|low|medium|high|elite|unknown
movement_lead: false
movement_rear: false
vulnerable_to: [daylight, silver]
carries: [knife]
Rules: a protected character at the rear (R1) or at the lead (R2) of the group warns. A protected character present with no protector positioned warns (R3). A lead position that does not match movement_lead: true warns (R4). Without tactical data there is only a note (R0). The tool also returns questions for the author as a checklist.
The optional knowledge block in a character's frontmatter assigns knowledge domains to the tiers expert, competent, layperson and none:
knowledge:
expert: [it, networking]
competent: [photography]
layperson: [history]
none: [forensics, medicine]
Specialist vocabulary of a domain that is none or layperson for the POV character produces a warning. Matches are searched as whole words. Free-form domain names count as competent. Without a knowledge block the character is not checked. Bundled vocabulary lists live under reference/craft/knowledge-domains/: forensics, tactical_combat, medicine, ballistics, automotive_repair.
analyze_plot_logic(book_slug, scope?, chapter_slug?) builds a knowledge index from canon facts, timeline and promises and runs two deterministic checks:
| Category | Check | Memoir |
|---|---|---|
causality_inversion |
A chapter refers to a fact whose chapter lies later in story time | active |
chekhov_gun |
A registered promise without payoff (manuscript scope only) | skipped |
The semantic categories information_leak, motivation_break, premise_violation and pov_knowledge_boundary are judged by the skills chapter-reviewer and manuscript-checker using the knowledge index. premise_violation does not apply to memoir.
Promises are setup elements (Chekhov elements, mysteries, callbacks) per chapter. register_chapter_promises writes them to the ## Promises section of the chapter README, get_chapter_promises reads them. For chapters already written, /storyforge:backfill-promises fills them in. The source of truth remains the canon log in the database (add_canon_fact).
Discoveries move across three levels: book, author, global. /storyforge:harvest-author-rules calls harvest_book_rules and sorts a book's rules into banned_phrase, style_principle and world_rule. For each candidate you decide: promote, keep in the book only, discard, or edit and promote. Promoted entries carry an origin such as _(emerged from firelight, 2026-04)_. If the same discovery shows up in a later book, a second origin is added instead of a duplicate. /storyforge:promote-rule lifts a single rule to author or global level.
The sections in the author profile are Recurring Tics, Style Principles and Don'ts. chapter-writer and chapter-reviewer load them through get_author.
run_pre_export_gates checks:
| Gate | Blocks |
|---|---|
All chapters Final |
yes |
| At least one chapter exists | yes |
| Word count at least 80% of target | no, warning only |
| Synopsis with more than 50 words | no, warning only |
With --arc, export-engineer may bypass the "All chapters Final" gate after explicit confirmation. Further rules of the skill:
check_memoir_consent runs first. On FAIL the export stops, on WARN the skill asks.export/front-matter.md (no [Author Name] placeholder, field author set).review_handle in the draft stop the export.get_cover_image returns one. Without a cover the export still runs.export/output/{book-slug}.{epub|pdf|mobi}, for an ARC {book-slug}-arc.{ext}.Besides the Markdown files, StoryForge uses SQLite databases under ~/.storyforge/db/:
| File | Content |
|---|---|
storyforge.db |
Session (active book, chapter, phase) |
authors.db |
Author discoveries (author_discoveries) and logged review feedback (review_feedback) |
{series-or-book-slug}.db |
Canon facts, character snapshots, book rules and callbacks, cover tracking |
The comparison baselines and raw drafts of fiction chapters under ~/.storyforge/snapshots/ are not a database, just plain files.
Moving from Markdown data to the databases is a one-time job using scripts in the plugin's scripts/ directory. Run them with the virtual environment's Python, in this order. All are repeatable, entries that already exist are skipped.
| Step | Script | Preview and execution |
|---|---|---|
1. Book rules and callbacks from CLAUDE.md |
scripts/migrate_book_rules.py |
--dry-run only shows. Without the flag it writes. --clear-markers then empties the marker sections. |
2. Canon log (plot/canon-log.md, people-log.md) |
scripts/migrate_canon_log_to_db.py |
Without a flag preview only, --execute writes. |
3. Writing discoveries from profile.md and character snapshots |
scripts/migrate_phase3.py |
--dry-run only shows. Without the flag it writes. |
4. Banned words from vocabulary.md |
scripts/migrate_vocabulary_to_db.py |
Without a flag preview only, --execute writes. |
5. Backfill source_genres |
Skill /storyforge:migrate-source-genres |
for authors who used study-author earlier |
For books with old rules in quotation marks instead of backticks there is python -m tools.claudemd.migrate_to_backticks <book-path>. Without --apply it only shows the change, with --apply it asks before writing. Run it from the plugin directory.
To backfill existing books, use the skills /storyforge:backfill-promises and /storyforge:backfill-style-principles.