Filter packs
Everything DaisyFilter blocks, censors, or reports lives in plugins/DaisyFilter/filters/. These
files are yours to edit — add words, remove words, correct a pattern, or delete a whole pack you do
not want.
Run /daisyfilter reload after every edit. A pack that fails to parse is skipped with a warning and
the previous rules stay live, so a typo can never take your filter offline.
Everything below assumes no regex knowledge. Where a pattern appears, the line above it says in plain words what it matches.
Add a word
Section titled “Add a word”Put it in 90-custom.yml, one term per line. That pack loads last and is never overwritten by an
update.
words: - examplewordone - examplewordtwoLeet and homoglyph variants are handled for you, so examplewordone already covers 3xampl3wordon3
and lookalike-letter spellings. You do not need to add them.
Any entry can override the pack defaults if one term deserves harsher treatment than the rest:
words: - examplewordone - word: examplewordtwo categories: [hate] severity: 5 disposition: BLOCK actions: [WARN, ALERT_STAFF, LOG]Reload, then confirm with /daisyfilter test examplewordone.
Remove a word
Section titled “Remove a word”There are three ways to stop a term being flagged, and they are not interchangeable.
| Situation | Do this |
|---|---|
| A word you added yourself is no longer wanted | Delete the line from your pack |
| You may want it back, or want to leave a note about why it is off | Set enabled: false on that entry |
| The word is genuinely harmless and should be permitted everywhere | Add it to allowlist.yml → terms |
Delete the line when the rule was yours and the decision is final. Use enabled: false when the
rule should stay visible in the file — it keeps the entry (and your comment explaining it) for the
next person who reads the pack.
Use allowlist.yml when the problem is not one rule but a word: an ordinary word that happens to
contain a blocked term inside it, such as a place name or a game term. An allowlisted term suppresses
matches inside it no matter which pack produced the match, which is exactly what you want for
something like assassin.
If you are not sure which of those you need, read Which whitelisting route do I need? below first.
Which whitelisting route do I need?
Section titled “Which whitelisting route do I need?”There are four ways to stop DaisyFilter reacting to something, and picking the wrong one either does not fix your problem or quietly opens a hole. Start here.
| You want to permit… | Use | Where it is recorded |
|---|---|---|
| a word, everywhere, for everyone | allowlist.yml → terms, or /daisyfilter allow <word> | allowlist.yml / runtime-overrides.yml |
| one bad pattern that keeps misfiring | disable that rule — /daisyfilter rule disable <id>, the GUI button, or enabled: false in the pack | runtime-overrides.yml / the pack |
| a person or a rank to skip filtering | a daisyfilter.bypass.* permission | your permission plugin |
| your own server address or a partner’s | allowlist.yml → domains | allowlist.yml |
Read the difference this way:
- Allowlist a word when the word itself is innocent and you never want to see it flagged again, whichever rule produced the match. An allowlisted term also suppresses matches inside it, which is how a place name or a game term containing a blocked fragment stops tripping the filter.
- Disable a rule when the word is not the problem — one specific pattern is too broad, and the
rest of the filter should carry on unchanged. Get the id from
/daisyfilter testor the incident detail, then/daisyfilter rule disable <id>./daisyfilter allowlistlists what you have disabled, and/daisyfilter rule enable <id>puts it back. - Grant a bypass permission when the exception is a person, not a word: a build-team account writing signs, or a bot. This is the bluntest instrument here — it skips scanning altogether for that surface, so never put one on a default rank on a server with children. See permissions.md.
- Allowlist a domain for your own address, your Discord, or a partner server. Link rules cannot
tell your IP from an advertiser’s, so anything you want said in chat has to be listed under
domains.
Fix a false positive on a bundled rule
Section titled “Fix a false positive on a bundled rule”Do not edit the shipped pack. Instead, define a rule with the same id in 90-custom.yml —
because that pack loads last, your version wins:
rules: # The shipped rule fires on a phrase our build-team players use legitimately. - id: nsfw-example-phrase type: REGEX pattern: '(?i)\bexamplewordone\b' match-mode: WHOLE_WORD disposition: MONITORYou can narrow the pattern, soften the disposition, or switch the rule off entirely with
enabled: false while keeping everything else in that pack intact.
If you need it stopped now, disable the single rule instead — /daisyfilter rule disable <id>, or
the button on the incident in the moderation GUI. Both are recorded in runtime-overrides.yml and
win last, so they survive a reload and take effect without anyone opening a file.
| Route | Use it when |
|---|---|
Override the id in 90-custom.yml | The fix is permanent and you want it written down and reviewable |
/daisyfilter rule disable <id>, or the GUI button | A false positive is disrupting chat right now and you want it off in seconds |
Add the word to allowlist.yml, or /daisyfilter allow <word> | A benign word keeps tripping several rules, not just one |
/daisyfilter filters verify shows which packs you have changed from the versions shipped in the jar,
and /daisyfilter filters reset <pack> restores one (backing yours up first) if an edit went wrong.
Close a bypass
Section titled “Close a bypass”When someone finds a way past a word — spacing it out, punctuating it, or writing an address the
generic link rules miss — a plain term will not catch it. Add a pattern to the relevant pack instead.
For server-specific bypasses that is 90-custom.yml:
rules: # A rival server's address typed with optional spaces or dots around the parts. - id: custom-server-ip type: REGEX pattern: '(?i)\bplay\s*\.?\s*rival\s*\.?\s*net\b' categories: [links] severity: 2 disposition: BLOCK match-mode: SUBSTRING
# The word "badword" typed with spaces, dots, hyphens, or underscores between every letter. - id: custom-spaced-bypass type: REGEX pattern: '(?i)\bb[\s.\-_]*a[\s.\-_]*d[\s.\-_]*w[\s.\-_]*o[\s.\-_]*r[\s.\-_]*d\b' categories: [swearing] match-mode: SUBSTRINGReading those: (?i) means ignore capitalisation, \b means the match must start and end at a word
boundary, \s* means “any amount of whitespace, or none”, and [\s.\-_]* means “any run of spaces,
dots, hyphens, or underscores, or none”. Always test a new pattern with /daisyfilter test against
both a message that should match and a message that should not.
Pack layout
Section titled “Pack layout”Packs load in filename order, which is why they are numbered. Later packs win, so a rule you define
in 90-custom.yml overrides a shipped rule with the same id.
| File | Contains |
|---|---|
00-child-safety.yml | Grooming, predatory behaviour, CSAM, sextortion, self-harm. Read the comments before editing. |
05-links.yml | URLs, bare domains, IPs, Discord invites |
10-personal-info.yml | Age, address, school, phone, email, and social-handle solicitation |
15-core-safety.yml | The reviewed Daisy baseline. Every field is written on each rule, so categories.yml cannot weaken these — they are the floor of protection. |
20-swearing.yml | Ordinary profanity |
30-nsfw.yml | Adult content |
40-hate.yml | Slurs and hate speech |
45-safety-risk.yml | Child-safety terms. Always blocked. |
50-multilingual.yml | Spanish, Portuguese, French, German profanity, slurs, and grooming |
60-other.yml | Harassment and context-dependent terms |
90-custom.yml | Yours. Never overwritten by an update. |
Every pack except 90-custom.yml is written on first start only if it is absent. An update never
overwrites a pack you already have, so your edits survive — which also means a new shipped rule
reaches you only in a pack file you have not touched. Use /daisyfilter filters verify to see which
of yours have diverged.
You are not limited to these names. The loader picks up filters/**/*.yml, so split, merge, rename,
or nest packs in subfolders however suits you — one pack per language, or a folder per moderator team.
Only the filename order matters, and later still wins.
Pack structure
Section titled “Pack structure”pack: id: nsfw # unique; used in logs and /daisyfilter filters description: Adult content enabled: true # false disables the whole pack without deleting it source: "Daisy curation" # provenance, kept for licence attribution defaults: # applied to every entry that does not override it categories: [nsfw] severity: 4 disposition: BLOCK actions: [WARN, ALERT_STAFF, LOG] match-mode: WHOLE_WORD forms: [PLAIN, LEET, STRICT] surfaces: [CHAT, COMMAND, SIGN, BOOK, ANVIL, NICKNAME, PLAYER_NAME]
words: # bulk literal terms, one per line - examplewordone
rules: # patterns, for anything a plain word cannot express - id: nsfw-example-phrase type: REGEX pattern: '(?i)\bexamplewordone\b' match-mode: SUBSTRINGEvery pack accepts both words: and rules:. Use words: for terms and rules: for patterns.
pack fields
Section titled “pack fields”| Field | Meaning |
|---|---|
id | Unique pack id. Appears in logs and /daisyfilter filters. |
description | Free text, for whoever reads the file next. |
enabled | false disables the whole pack without deleting it. |
source | Provenance, kept for licence attribution. |
defaults | Any of the entry fields below, applied to every entry that does not set its own. |
Entry fields
Section titled “Entry fields”Valid on any words: or rules: entry, and inside pack.defaults.
| Field | Meaning |
|---|---|
id | Unique rule id. Appears in /daisyfilter test, incident details, and the API. |
type | LITERAL (a term) or REGEX (an RE2 pattern). Defaults to LITERAL for words:, REGEX for rules:. |
pattern / word | The term or pattern to match. |
categories | One or more categories. Drives categories.yml policy for anything left unset. |
severity | 1–5. Severity ≥ 4 bypasses the staff-alert grouping cooldown. |
disposition | BLOCK (withhold), CENSOR (star out), MONITOR (allow, record, alert), ALLOW (ignore). |
actions | WARN, ALERT_STAFF, LOG, TEMP_MUTE, KICK, RUN_COMMAND. |
match-mode | WHOLE_WORD only matches a standalone word; SUBSTRING matches inside longer words. |
forms | Which normalised forms to match — see the table below. Defaults to [PLAIN]. |
match-original | REGEX only. Match against the raw message as typed, preserving the . / @ that normalisation collapses — required for link, email, and handle patterns. |
surfaces | Where the rule applies: CHAT, COMMAND, SIGN, BOOK, ANVIL, NICKNAME, CUSTOM, PLAYER_NAME. Omit to apply everywhere. |
enabled | false keeps the rule in the file but switches it off. |
Every message is rewritten into four forms before matching. Listing a form on a rule means “also try to match against this version of the message”.
| Form | What it is |
|---|---|
PLAIN | As typed, lowercased, accents and invisible characters stripped, runs of spaces or punctuation collapsed to one. |
COMPACT | PLAIN with every space and punctuation mark removed. |
LEET | PLAIN with digit and symbol substitutions folded back to letters: 0→o, 1→i, 3→e, 4→a, 5→s, 6→g, 7→t, 8→b, 9→g, @→a, $→s, !→i, |→i, +→t. |
STRICT | LEET, plus lookalike characters from other alphabets folded to ASCII, every non-letter/digit dropped, and runs of three or more identical characters collapsed to one. |
The shipped packs use [PLAIN, LEET, STRICT], which is what makes the leet and lookalike coverage
work without you writing out variants. STRICT is the broadest form and therefore the most likely to
produce a false positive on a very short term; if a three-letter word you add starts matching inside
ordinary chat, narrow it to forms: [PLAIN] on that entry.
How a rule’s final policy is decided
Section titled “How a rule’s final policy is decided”-
Pack
defaults:supply a baseline. -
Anything written on the entry itself overrides those defaults — and is marked as yours.
-
categories.ymlthen applies, but it can only touchseverity,disposition, andactionsthat you did not write on the entry:- A value you typed on the entry is authoritative. A category cannot weaken it.
- A value inherited from the pack’s
defaults:block is still fair game, which is what lets you relax a whole category without editing every rule in it. severityis a floor either way: a rule that ships higher keeps its own.enabled: falseon a category switches off every rule in it regardless.
See categories.md.
-
allowlist.ymlplus anything added with/daisyfilter allowsuppresses matches inside permitted words and domains. -
Rules disabled from the moderation GUI or with
/daisyfilter rule disableare recorded inruntime-overrides.ymland win last.
For a rule that does not state a disposition and belongs to several categories, the strongest
disposition among them applies (BLOCK > CENSOR > MONITOR > ALLOW) and the effects are the union. A
category that categories.yml does not define changes nothing at all.
This is why the rules in 15-core-safety.yml write every field out explicitly: it makes them immune
to a categories.yml edit, so they act as the floor of protection whatever else you tune.
Patterns are RE2, not Java regex
Section titled “Patterns are RE2, not Java regex”Patterns compile with RE2/J, which guarantees linear-time matching — a badly written pattern cannot
hang your server, however much text is thrown at it. The trade-off is that lookahead ((?=),
lookbehind ((?<=), negative lookahead ((?!), and backreferences are not supported.
A pattern using them, or one with a plain syntax error, is rejected when the pack loads. You get a
console warning naming the rule, that one rule is disabled, and the rest of the pack still loads
normally. Check /daisyfilter filters after a reload to see whether anything failed.
Before you trust an edit
Section titled “Before you trust an edit”Take care with
00-child-safety.yml. Those patterns detect grooming, predatory contact, and sextortion. They are deliberately tuned so ambiguous phrasing only alerts staff while unambiguous phrasing blocks. Widen one and you should expect more false alerts; narrow one and you may lose a real signal. Change them deliberately.
Verify every edit with /daisyfilter test <message>. It scans the message with no side effects and
shows exactly which rule fired, the resulting disposition and severity, and the confidence behind it.
Test one message that should be caught and one similar message that should not — that second test is
how you catch a pattern that is too broad before your players do.
One caveat: test scans on the CUSTOM surface. A rule that lists specific surfaces: and does not
include CUSTOM will not fire in a test, even though it is loaded and working. Omit surfaces:
while you are testing a new rule.
Commands
Section titled “Commands”| Command | Does |
|---|---|
/daisyfilter filters | List packs, rule counts, and any that failed to load |
/daisyfilter filters verify | Show which packs differ from the versions shipped in the jar |
/daisyfilter filters reset <pack> | Restore one pack to the shipped version (backs yours up first) |
/daisyfilter test <message> | Scan a message and see exactly which rule fired |
/daisyfilter allowlist | Show allowed terms and any rules you have disabled |
/daisyfilter allow <term> | Permit a word or domain everywhere, without editing a file |
/daisyfilter unallow <term> | Undo an in-game allow |
/daisyfilter rule <enable|disable> <id> | Switch one rule off or back on by its id |
/daisyfilter reload | Re-read every pack |
See commands.md for the full command list and configuration.md for the other configuration files.