StoryForge has several places where banned phrases and patterns live, and two scanners that read them: the prose hook and the manuscript checker. This page is the map: what lives where, who scans what, in which format and at which level.
If you wonder where a new rule belongs, jump to the decision tree.
| Scanner | When | Mechanics | Consequence |
|---|---|---|---|
Prose hook (hooks/validate_chapter.py) |
PostToolUse: after every Write, Edit and MultiEdit on draft.md |
The file is already written. Severity block gives exit code 2 in strict mode and feeds the message back to the model through stderr |
the model has to fix the hit before it continues |
Manuscript checker (/storyforge:manuscript-checker) |
called by you, scans the whole book | report at research/manuscript-report.md and a gate result in the tool response |
does not block any write, see below |
The hook checks chapter by chapter right after writing, the checker the finished book. Both read the same sources and use mostly the same category names, but report with their own levels. The one difference for catalog words: the hook calls the category ai_tell, the checker ai_tell_violation.
The manuscript checker's gate is FAIL when there is a book_rule_violation finding whose rule documents no exception of its own. The gate is not in the Markdown report, only in the gate key of the scan_manuscript tool response. The manuscript checker is not part of run_pre_export_gates, so a FAIL does not stop an export by itself. Both scanners also read storage G, the tone rules from the book's plot/tone.md.
| # | Place | Where | Content |
|---|---|---|---|
| A | Book | database of the series or book (book rules, type rule) |
rules of the book, also visible in CLAUDE.md under ## Rules (from DB) |
| B | Author | ~/.storyforge/db/authors.db, type donts |
banned phrases, patterns and don'ts of the author |
| C | Author | authors.db, type recurring_tics |
recurring tics of the author |
| D | Author | authors.db, type style_principles |
positive style patterns, not scanned |
| E | Catalog | reference/craft/anti-ai-patterns.md |
section 1: ### Heavily Flagged Words and Phrases |
| F | Catalog | reference/craft/anti-ai-patterns.md |
section 11: lines with **Banned shape:** |
| G | Book | plot/tone.md, "Banned Prose Patterns" section |
banned patterns of the book's tone brief |
An author's vocabulary.md file is reference only and is not scanned. Banned words that sit in such a file are carried into storage B by scripts/migrate_vocabulary_to_db.py. create_author additionally seeds 19 default AI-tell words (delve, tapestry, nuanced, ...) straight into B as flat phrases ā they take effect immediately as author_vocab_violation and no longer appear in vocabulary.md. If that database write fails, the author is still created; create_author's response then carries a warning field pointing to add_vocabulary_entry to add them afterward.
For storage B and C to apply to a book, the hook and the manuscript checker first have to resolve its author: they read the - **Author:** ... line from the book's CLAUDE.md and slugify the name. When that line is missing ā the standard workflow never writes it ā both fall back to the author: field in the README.md YAML frontmatter instead, which create_book_structure sets on every book. Without either source, B and C stay empty for that book.
| Storage | Hook | Hook level | Hook category | Manuscript checker | MC weight | MC category |
|---|---|---|---|---|---|---|
| A book rules | yes, backticks only | block | book_rule_violation |
yes, backticks and quotes with a ban cue | high | book_rule_violation |
| B author don'ts, flat phrases | yes | block | author_vocab_violation |
yes | high | author_vocab_violation |
| B author don'ts, bold title with no pattern (fallback) | yes | block | author_vocab_violation |
yes | high | author_vocab_violation |
| B author don'ts, backtick and italic | yes | block | author_rule_violation |
yes | high | author_rule_violation |
| C author tics | yes | block or warn | writing_discovery_violation |
yes | medium or high | writing_discovery_violation |
| D style principles | no | no | ||||
| E catalog section 1 | yes | warn | ai_tell |
yes | medium | ai_tell_violation |
| F catalog section 11 | yes | warn | global_shape_violation |
yes | medium | global_shape_violation |
| G tone rules (book) | yes, backticks only | block | book_rule_violation |
yes, like a book rule | high | book_rule_violation |
The hook reports the catalog layers (E, F) as warn, the manuscript checker as medium. The catalog layers E and F apply only to books written in English, see the E and F subsections below.
The manuscript checker's report sorts every finding into a fixed order (_CATEGORY_RANK): first the two meta-findings book_rules_unreadable and callbacks_unreadable ā they signal that the book-rules or callback database itself could not be read (e.g. a series book not yet linked via add_book_to_series), with no text findings of their own. Then come book_rule_violation, author_rule_violation, author_vocab_violation, writing_discovery_violation, global_shape_violation, ai_tell_violation, anonymization_leak, plot_hole and cliche. Everything else is appended by severity.
Every storage has its own parser rules. A pattern in the wrong format is silently ignored.
A rule is a text. The hook pulls out only backtick patterns, the checker more:
| Form | Hook | Manuscript checker |
|---|---|---|
backtick regex (`\b...\b`) |
yes, hard | yes |
backtick literal (`delve`) |
yes, hard | yes |
quoted phrase with a ban cue (Never, Avoid, nie, kein ...) in the same sentence |
no | yes, if at least six characters |
italic (*phrase*) |
no | no (treated as a narrative example) |
| plain text | no | no |
A backtick text with regex characters (| ( ) [ ] \ ^ $ ? + * { }) is compiled as a regular expression ā through compile_backtick_pattern, a single function shared by the hook and the manuscript checker, applying the same way to book rules (A), author don'ts (B) and author tics (C). It has a guard against costly expressions: the text becomes a literal only when a quantified group (+, * or an open {n,}) itself carries an unbounded quantifier inside its own body, for example (a+)+. Flagging every quantified group, not only nested ones, would also catch harmless expressions: (foo)+ stays a regex because the group's body itself repeats nothing, and (?:being){2} stays a regex because {2} is a fixed upper bound, not an open {n,}.
Per-chapter cap: an addition such as max N per chapter activates a per-chapter cap. This applies to book rules (A) and author tics (C), not to don'ts (B). In the hook the limit is scaled by the length of the current draft relative to the chapter's target word count. The manuscript checker does not know this limit for book rules and reports all hits there; for tics (C) it does evaluate it, see section C.
An entry can be flat (one phrase), carry only a bold title with no pattern, carry backticks, or carry italic examples with a ban cue. The hook and the checker extract to different extents:
| Form | Hook | Manuscript checker |
|---|---|---|
flat phrase without backticks and bold (aliases with /, parenthetical notes are removed) |
yes, with inflections and a word boundary at the start | yes |
| line that starts with a bold title, with no backtick and no italic example phrase after a ban cue (falls back to the title text) | yes, like a flat phrase: with inflections and a word boundary at the start | yes, like a flat phrase |
| backtick regex or literal | yes, always; a literal is a plain substring without inflection and word boundary | yes, always |
| italic with a ban cue | yes, only before a recommendation marker | yes, only before a recommendation marker |
| quote with a ban cue | no | yes, only before a recommendation marker |
Entries written by write_author_banned_phrase are backtick literals: `delve` matches redelve, but not delving. Inflection and word boundary exist only for flat entries (add_vocabulary_entry, migration from vocabulary.md) and for the bold-title fallback.
A donts row with a bold title but no backtick and no italic example phrase after a ban cue falls back to the title text itself, provided the bold sits directly at the start of the line: load_author_vocab reads it like a flat phrase. That way every such row is enforced by exactly one of the two loaders ā either here as a fallback literal, or, when the entry does carry a backtick pattern or an italic example phrase with a ban cue, by load_author_dont_rules (next row below).
For italic patterns, trailing punctuation is stripped so that *The room received it.* also hits The room received it without complaint.
Recommendation markers: Render, Replace, Use instead, Instead:, Allowed:, Better:, Rewrite as, Rather: and ā. The marker window (extract_ban_window) is the same function in the hook and the manuscript checker: both extract italic phrases only from the slice before the first marker, after which they count as positive examples. The checker additionally applies the window to quotes; the hook does not extract quotes for don'ts at all (see the table above). Backticks are extracted by both scanners regardless of position.
- **Never personify rooms** ā *The room received it.* / *the silence held it.*
Render the impact through a named body: *Caelan's eyes did not move*
In the example, both scanners treat only The room received it. and the silence held it. as bans; Caelan's eyes did not move follows Render and counts as a recommendation, in the hook too. For recommendations inside don'ts that means: put them after one of the markers ā whether they sit in italics there or not no longer matters for extraction.
A bullet with a bold title. The phrase is searched in this order:
| Priority | Form | When |
|---|---|---|
| 1 | quote in the bold title (**Vague-noun "thing" as fallback**) |
main path |
| 2 | backticks or quotes in the bullet text | when the title has no quote |
| 3 | text of the bold title | when neither title nor text carry anything scannable |
That way German rule titles with English example phrases in the text work. Additions in the bullet control the level in the hook: [warn] or [advisory] turns block into warn, Max N per chapter (also Max NāM pro Kapitel, Max einmal pro Kapitel) allows up to N hits per chapter, scaled to the length of the current draft. The manuscript checker now evaluates both additions too: [warn]/[advisory] gives weight medium there instead of high, and a chapter at or under the cap produces no finding at all ā evaluated per chapter of the finished draft, not against the manuscript-wide total.
Not machine-scanned, deliberately. They are narrative instructions to chapter-writer and chapter-reviewer, not phrase banlists. They are not part of the chapter writing brief: both skills load them through get_author.
A numbered list under ### Heavily Flagged Words and Phrases. Each entry sets its terms in bold, followed by the reason after a dash:
1. **Delve** / **Delve into** ā One of the most notorious AI tells.
2. **Tapestry** (metaphorical) ā "A rich tapestry of..." Almost never used by humans.
Each bold term is read separately, so Delve and Delve into. The / between the bold terms is only notation: the loader does not split inside a bold term at /, so every term must carry its own **. A parenthetical note such as (metaphorical) outside the bold is not read. If a parenthetical note sits inside the bold term, the loader removes it. Single words match inflections. Level: warn in the hook, medium in the checker. They are hints, not a user-asserted rule. Applies only to books written in English (language from language in the README frontmatter); for other languages there is currently no catalog, so these hits are dropped.
Only the split at / exists exclusively for flat don'ts (B). The loader also removes parenthetical notes for flat don'ts, for bold catalog terms and for tic phrases.
Lines in section 11 with this exact form:
**Banned shape:** `\bregex_here\b`
Only the text in backticks after **Banned shape:** is compiled as a regular expression. One regex per shape. Level: warn in the hook, medium in the checker. An invalid expression is skipped. Lines with an addition such as **Banned shape (density):** do not match this form and are not loaded; at present six lines are loaded. Applies only to books written in English (language from language in the README frontmatter); for other languages there is currently no catalog, so these hits are dropped.
The "Banned Prose Patterns" section in a book's plot/tone.md, heading at any level from ## to ######. Bullets with a phrase in backticks are enforceable, using the same compile_backtick_pattern as A, B and C; an addition such as max N per chapter caps hits per chapter exactly like book rules. Bullets without backticks stay a plain hint that no scanner enforces ā visible to reviewers and the humanizer, not to the hook or the manuscript checker.
When you create a discovery with write_author_discovery(validate=True), a section-aware check runs and attaches warnings and extracted_patterns to the response. The write always goes through. The check shows what the scanner will actually enforce, and it follows the extractor of the manuscript checker:
| Section | Code | Finding |
|---|---|---|
donts |
mixed_positive_negative_italics |
italics on both sides of a recommendation marker. Italic phrases after the marker are not extracted as a ban by either scanner ā the check surfaces this asymmetry so you can verify they really are meant as a recommendation. |
donts |
mixed_positive_negative_quotes |
several quotes under one ban cue |
donts |
scanner_extracts_nothing |
ban cue without backtick, quote or italic, the rule does not take effect |
donts, recurring_tics, style_principles |
bracket_placeholder |
[word] in backticks, which is a character class and not a placeholder |
recurring_tics |
bold_title_unscannable |
non-ASCII title without a pattern in the text |
Style principles deliberately produce no gap warnings. For book rules the same idea exists with lint_book_rules and /storyforge:rules-audit, see Quality System.
Where do I put a pattern that should be blocked or reported?
Is it a universal AI tell that affects every author?
delve, tapestry): E, catalog section 1Does it apply to all books of one author, so it is a trait of their voice?
write_author_banned_phrase)harvest-author-rules or write_author_discovery with recurring_tics)Does it apply to this one book only?
max N per chapter): A (append_book_rule)plot/tone.md)Often misassigned:
| Temptation | Correct |
|---|---|
| "AI tell, so a book rule" | catalog (E or F) |
"Voice trait of the author, so vocabulary.md" |
The file is not read. A phrase or shape belongs in B, a recurring tic in C. |
| "A style principle as a scannable pattern" | D is not scanned. If it should block, it is a don't in B. |
When the same pattern exists in several places:
| Case | Result |
|---|---|
| book rule (A) and author entry (B or C) | both report, the categories are independent |
| author entry (B, C) and catalog (E, F) in the hook | both report: the author entry as block, the catalog as warn |
| author entry and catalog in the manuscript checker | the catalog finding (ai_tell_violation, global_shape_violation) is dropped on the line that an author entry already hits |
| flat phrase (B), tic (C) and don't (B) in the hook | the same phrase only once: flat phrase first, then tic, then don't |
| book rule (A) and tone rule (G) with the same pattern in the hook | reports only once |
Dedup keeps no mute lists. If a catalog pattern reports falsely, you can only narrow it in the catalog (a change to the plugin) or deliberately not copy it into an author don't, so it stays at warn.
linter_modereference/craft/anti-ai-patterns.md in the plugin: storage E and F