Skip to content

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.

Put it in 90-custom.yml, one term per line. That pack loads last and is never overwritten by an update.

words:
- examplewordone
- examplewordtwo

Leet 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.

There are three ways to stop a term being flagged, and they are not interchangeable.

SituationDo this
A word you added yourself is no longer wantedDelete the line from your pack
You may want it back, or want to leave a note about why it is offSet enabled: false on that entry
The word is genuinely harmless and should be permitted everywhereAdd it to allowlist.ymlterms

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.

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…UseWhere it is recorded
a word, everywhere, for everyoneallowlist.ymlterms, or /daisyfilter allow <word>allowlist.yml / runtime-overrides.yml
one bad pattern that keeps misfiringdisable that rule — /daisyfilter rule disable <id>, the GUI button, or enabled: false in the packruntime-overrides.yml / the pack
a person or a rank to skip filteringa daisyfilter.bypass.* permissionyour permission plugin
your own server address or a partner’sallowlist.ymldomainsallowlist.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 test or the incident detail, then /daisyfilter rule disable <id>. /daisyfilter allowlist lists 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.

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: MONITOR

You 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.

RouteUse it when
Override the id in 90-custom.ymlThe fix is permanent and you want it written down and reviewable
/daisyfilter rule disable <id>, or the GUI buttonA 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.

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: SUBSTRING

Reading 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.

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.

FileContains
00-child-safety.ymlGrooming, predatory behaviour, CSAM, sextortion, self-harm. Read the comments before editing.
05-links.ymlURLs, bare domains, IPs, Discord invites
10-personal-info.ymlAge, address, school, phone, email, and social-handle solicitation
15-core-safety.ymlThe reviewed Daisy baseline. Every field is written on each rule, so categories.yml cannot weaken these — they are the floor of protection.
20-swearing.ymlOrdinary profanity
30-nsfw.ymlAdult content
40-hate.ymlSlurs and hate speech
45-safety-risk.ymlChild-safety terms. Always blocked.
50-multilingual.ymlSpanish, Portuguese, French, German profanity, slurs, and grooming
60-other.ymlHarassment and context-dependent terms
90-custom.ymlYours. 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:
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: SUBSTRING

Every pack accepts both words: and rules:. Use words: for terms and rules: for patterns.

FieldMeaning
idUnique pack id. Appears in logs and /daisyfilter filters.
descriptionFree text, for whoever reads the file next.
enabledfalse disables the whole pack without deleting it.
sourceProvenance, kept for licence attribution.
defaultsAny of the entry fields below, applied to every entry that does not set its own.

Valid on any words: or rules: entry, and inside pack.defaults.

FieldMeaning
idUnique rule id. Appears in /daisyfilter test, incident details, and the API.
typeLITERAL (a term) or REGEX (an RE2 pattern). Defaults to LITERAL for words:, REGEX for rules:.
pattern / wordThe term or pattern to match.
categoriesOne or more categories. Drives categories.yml policy for anything left unset.
severity1–5. Severity ≥ 4 bypasses the staff-alert grouping cooldown.
dispositionBLOCK (withhold), CENSOR (star out), MONITOR (allow, record, alert), ALLOW (ignore).
actionsWARN, ALERT_STAFF, LOG, TEMP_MUTE, KICK, RUN_COMMAND.
match-modeWHOLE_WORD only matches a standalone word; SUBSTRING matches inside longer words.
formsWhich normalised forms to match — see the table below. Defaults to [PLAIN].
match-originalREGEX only. Match against the raw message as typed, preserving the . / @ that normalisation collapses — required for link, email, and handle patterns.
surfacesWhere the rule applies: CHAT, COMMAND, SIGN, BOOK, ANVIL, NICKNAME, CUSTOM, PLAYER_NAME. Omit to apply everywhere.
enabledfalse 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”.

FormWhat it is
PLAINAs typed, lowercased, accents and invisible characters stripped, runs of spaces or punctuation collapsed to one.
COMPACTPLAIN with every space and punctuation mark removed.
LEETPLAIN 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.
STRICTLEET, 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.

  1. Pack defaults: supply a baseline.

  2. Anything written on the entry itself overrides those defaults — and is marked as yours.

  3. categories.yml then applies, but it can only touch severity, disposition, and actions that 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.
    • severity is a floor either way: a rule that ships higher keeps its own.
    • enabled: false on a category switches off every rule in it regardless.

    See categories.md.

  4. allowlist.yml plus anything added with /daisyfilter allow suppresses matches inside permitted words and domains.

  5. Rules disabled from the moderation GUI or with /daisyfilter rule disable are recorded in runtime-overrides.yml and 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 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.

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.

CommandDoes
/daisyfilter filtersList packs, rule counts, and any that failed to load
/daisyfilter filters verifyShow 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 allowlistShow 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 reloadRe-read every pack

See commands.md for the full command list and configuration.md for the other configuration files.