Generated by php flatbb hooks:list on 2026-09-30. Do not edit by hand.
Filters receive ($value, array $ctx) and return the new value (or null to keep it). Events receive (null, array $ctx).
Regions are filters named region.<position> whose value is HTML (or an array for list regions). Inline regions run inside loops: no database queries in their callbacks.
| Hook | Kind | Description | Where |
|---|---|---|---|
account.after_login | event | After a successful login. | app/account.php |
account.after_register | event | After a new account was created. | app/account.php |
account.login_challenge | filter | After the password was checked: return a URL to send the user to a second step (two-factor) instead of signing them in; the plugin finishes with login_pending_user() + login_user(). | app/account.php |
account.login_validate | filter | Add errors to a sign-in attempt before the password is checked (ctx: username); a non-empty list refuses it. | app/account.php |
account.register_validate | filter | Add validation errors to registration. | app/account.php |
admin.action | event | After an admin action was logged (ctx: user_id, ip, action, target, detail). The security plugin subscribes here. | core/security.php |
admin.category_after_save | event | After a category was created or edited (ctx: id, data); plugins keep their own per-category options here. | app/admin_content.php |
admin.category_save | filter | Filter category data before saving. | app/admin_content.php |
admin.plugin_ops | filter | Extra buttons on a plugin row. | app/admin_system.php |
admin.plugin_settings.before | filter | HTML at the top of a plugin settings drawer (ctx: id, manifest). | app/admin_system.php |
admin.settings_fields | filter | Add a settings section: $value['myid'] = ['Label', ['key' => [type, label, help, options, min, max]]]. Each section is a tab in Admin → Settings. | app/admin.php |
admin.settings_save | filter | Filter settings before they are saved. | app/admin.php, app/admin_ai.php |
admin.tool | event | Handle a custom tool action. | app/admin_system.php |
admin.tools | filter | Add rows to Admin → Tools. | app/admin_system.php |
admin.user_saved | event | After an admin edited a user. | app/admin.php |
ai.request | filter | Filter: a request to the shared AI connection before it is sent (value: system, user, opts; ctx: purpose = the calling plugin). Change it, or return an array with an error key to refuse it (quotas, redaction). | core/ai.php |
ai.response | event | Event: after each attempt on an AI connection (ctx: purpose, connection = 1 for the main one, 2 and 3 for the backups, model, usage [in, out] tokens, ok). A request that falls back fires it once per connection tried. For plugins that count or log usage. | core/ai.php |
api.<action> | filter | Handle /api/<action>; return an array to respond as JSON. | app/api.php |
app.boot | event | Every request after plugins are loaded. Preload data here. | core/boot.php |
auth.login.after | filter | HTML below the sign-in form. | app/views/login.php |
auth.register.after | filter | Html under the registration form (social sign-up buttons). | app/views/register.php |
composer.values | filter | Filter the values a composer opens with: title, body, category_id, tags for a new topic (ctx mode new), body for a reply (ctx mode reply, topic). Used by drafts plugins. | app/topic.php, app/views/topic.php |
cron.jobs | filter | Filter the list of scheduled jobs. | core/cron.php |
editor.emoji | filter | app/views/editor.php | |
editor.help | filter | app/views/editor.php | |
editor.options | filter | app/views/editor.php | |
feed.topics | filter | app/api.php | |
icon.paths | filter | Add SVG icons: name => path markup. | core/icons.php |
import.done | event | core/import.php | |
import.reset | event | core/import.php | |
link.previewable | filter | Filter: whether a link may get a preview card (bool; ctx url, host). Return false to keep a link a link, e.g. when your plugin shows its own player for that site. | core/links.php |
mail.send | filter | core/helpers.php | |
markdown.after | filter | Rendered HTML of a post. | core/markdown.php |
markdown.before | filter | Markdown source before rendering. | core/markdown.php |
markdown.excerpt | filter | Filter: the Markdown a plain excerpt is made from (ctx: max): search index, page descriptions, notifications, feeds. Strip content not every reader may see. | core/markdown.php |
menus.known | filter | Filter: the list regions an admin edits under Admin → Appearance → Menus (region => name). Add your plugin's own list region to make its items editable. | core/hook.php |
migrate.after_import | event | core/migrate.php | |
notification.after_create | event | After a notification was stored. | app/notification.php |
notification.after_create_many | event | After notify_many() stored the same notification for many members (ctx: user_ids, from_user_id, kind, topic_id, post_id). | app/notification.php |
notification.before_create | filter | Filter/veto a notification (return null to skip). Also runs once per recipient inside notify_many() (ctx: many = true): no database queries. | app/notification.php |
notification.kinds | filter | Icon and verb per notification kind (kind => [icon, verb]); plugins register their own kinds. | app/views/notifications.php |
notifications.rows | filter | Filter rows on the notifications page. | app/notification.php |
page.before_output | filter | The whole HTML document before it is sent. Use for page-level placeholder replacement. | core/render.php |
page.options | filter | Filter the page() options (left/right columns, class, description, canonical, robots, breadcrumbs, title_full = the complete <title> text). | core/render.php |
permissions.known | filter | Add permission keys shown in Admin → Groups. | app/admin.php |
plugin.after_install_zip | event | core/plugin.php | |
plugin.settings_saved | event | After plugin settings were saved (ctx: id). | app/admin_system.php, app/admin_theme.php |
points.after_change | event | After points were added or removed (ctx: user_id, delta, reason, ref_id, balance). | core/points.php |
points.before_change | filter | Filter or veto a points change (return false to block). | core/points.php |
points.icon | filter | The icon name of a points history line (ctx: reason, delta). Runs inside lists: no database access. | core/points.php |
points.reasons | filter | Register point reason codes: $value['checkin'] = 'Daily check-in'. Labels show in the private history. | core/points.php |
points.ref_url | filter | core/points.php | |
points.rules | filter | core/points.php | |
post.after_delete | event | After a reply was deleted or restored. | app/topic.php |
post.after_like | event | After a like toggle. | app/topic.php |
post.after_save | event | After a post was created or edited. A new reply that waits in the review queue fires it on approval. | app/topic.php |
post.before_save | filter | New reply data (body, reply_to_id) before insert. | app/topic.php |
post.before_update | filter | Reply body before an edit is saved. | app/topic.php |
region.<action> | filter | core/hook.php, core/render.php | |
region.admin.category.fields | html region | Extra fields at the end of the category editor (ctx: category) | app/admin_content.php |
region.admin.dashboard.cards | list region | Admin dashboard cards (list) | app/admin.php |
region.admin.menu | list region | Admin menu items (list) | app/admin_ui.php |
region.admin.plugins.tabs | list region | The tab row of Admin → Plugins: Installed plus what plugins add (list; ctx: active) | app/admin_system.php |
region.admin.themes.tabs | list region | app/admin_theme.php | |
region.auth.login.extra | html region | Inside the sign-in form | app/views/login.php |
region.auth.register.extra | html region | Inside the registration form | app/views/register.php |
region.body.end | html region | Before </body> (scripts) | app/views/layout.php |
region.composer.extra | html region | Extra fields inside the post/reply form | app/views/topic.php, app/views/topic_form.php |
region.composer.modes | list region | Editor mode tabs above the toolbar (list: label, icon, cmd, class): Write and Preview; a visual editor adds its own | app/views/editor.php |
region.composer.toolbar | list region | Editor toolbar buttons (list) | app/views/editor.php |
region.footer.left | html region | Footer, left | app/views/layout.php |
region.footer.links | list region | Footer links (list) | app/views/layout.php |
region.footer.right | html region | Footer, right | app/views/layout.php |
region.head | html region | Inside <head> (meta tags, analytics) | app/views/layout.php |
region.header.left | html region | Header, right of the logo | app/views/layout.php |
region.header.nav | list region | Header navigation links (list) | app/views/layout.php |
region.header.right | list region | Header, right side: search, new topic, language, theme, notifications, account menu (list) | core/render.php |
region.header.right.after_search | html region | Header, right of the search box | core/render.php |
region.header.right.before_search | html region | Header, left of the search box | core/render.php |
region.header.user_menu | list region | The account list: the header dropdown shows all of it, the sidebar member card the "you" items as shortcuts (list: label, url, icon, count, group "you" or "site", weight, card => false keeps one out of the card) | core/render.php |
region.header.user_menu.labels | html region | User dropdown, under the name next to the group label: short labels such as the level (ctx: user) | app/views/user_menu.php |
region.header.user_menu.stats | list region | User dropdown numbers strip (list: label, value, url; ctx: user) | app/views/user_menu.php |
region.main.after | html region | Below the main content on every page | app/views/layout.php |
region.main.before | html region | Above the main content on every page | app/views/layout.php |
region.main.categories | list region | Category bar above the list tabs: All + top-level categories (list) | app/home.php |
region.main.tabs | list region | Tabs above topic lists (list) | app/home.php |
region.main.toolbar | html region | Right of the list tabs; its New Topic button is hidden on wide screens where the member card shows its own | app/home.php |
region.member.actions | list region | Buttons of a member: the sidebar member card (New Topic first), the topic author card, the profile card and the account menu, under its numbers (list: label, url, icon, primary, count, title, done, weight; post: the button POSTs to url with the CSRF token and back = the current path; ctx: user, self, place card | author |
region.member.labels | html region | Short labels after the group label, wherever the core shows a member: the sidebar member card, the topic author card, the account menu and the profile (ctx: user, self, place card | author |
region.member.sections | list region | Sections beside the lists on a profile, one card each: badges, a calendar, a showcase (list: title, html, url, link, weight; ctx: user, self, place profile) | app/views/profile.php |
region.member.stats | list region | Numbers of a member, in the sidebar member card, the topic author card, the account menu strip and the profile card (list: label, value, url, sub, progress 0..1 draws a bar, weight; ctx: user, self, place card | author |
region.points.actions | list region | Ways to earn on the /points page, as tiles (list: label, title, sub, url, icon, primary, done; ctx: user) | app/points.php |
region.points.summary | list region | Short facts next to the balance on the /points page, such as the level (list: label, sub, progress 0..1, url; ctx: user) | app/points.php |
region.post.actions | list region | Post action buttons (list, loop, no DB) | app/views/post.php |
region.post.after | inline region (loop, no DB) | After each post (loop, no DB) | app/views/post.php |
region.post.before | inline region (loop, no DB) | Before each post (loop, no DB) | app/views/post.php |
region.post.content_after | inline region (loop, no DB) | After each post body (loop, no DB) | app/views/post.php |
region.post.meta | inline region (loop, no DB) | Post meta line, after the time: level, title, badges (loop, no DB) | app/views/post.php |
region.post.name | inline region (loop, no DB) | Post name row, after the username, OP and group labels: a short label such as the level (loop, no DB) | app/views/post.php |
region.sidebar.left.bottom | html region | Left column, bottom | app/views/sidebar_left.php |
region.sidebar.left.nav | list region | Left column navigation links, one per plugin (list: label, url, icon, badge, active, weight; group "community", "tools" or your own id with group_label; past setting nav_visible links the rest fold under More) | app/views/sidebar_left.php |
region.sidebar.left.top | html region | Left column, top | app/views/sidebar_left.php |
region.sidebar.right.bottom | html region | Right column, bottom | app/views/sidebar_right.php |
region.sidebar.right.cards | list region | Right column card stack (list) | core/render.php |
region.sidebar.right.top | html region | Right column, top | app/views/sidebar_right.php |
region.topic.actions | list region | Topic page action buttons (list) | app/views/topic.php |
region.topic.header | html region | Topic page, below the title block | app/views/topic.php |
region.topic.no_access | html region | Topic page when a plugin refused the visitor, under the reason | app/topic.php |
region.topic.replies_after | html region | After the post stream, before the reply box | app/views/topic.php |
region.topic.sidebar.bottom | html region | Topic page right column, bottom | app/views/sidebar_topic.php |
region.topic.sidebar.cards | list region | Topic page right column cards (list) | app/topic.php |
region.topic.sidebar.top | html region | Topic page right column, top | app/views/sidebar_topic.php |
region.topic_list.after | html region | After the topic list, before pagination | app/views/topic_list.php |
region.topic_list.before | html region | Between the list tabs and the topic rows (announcements, notices) | app/views/topic_list.php |
region.topic_list.item.after | inline region (loop, no DB) | After each topic row (loop, no DB) | app/views/topic_rows.php |
region.topic_list.item.meta | inline region (loop, no DB) | In each topic row meta line (loop, no DB) | app/views/topic_rows.php |
region.topic_list.item.title_suffix | inline region (loop, no DB) | After each topic title (loop, no DB) | app/views/topic_rows.php |
region.user.moderate.options | html region | app/moderation.php | |
region.user.profile.actions | html region | app/views/profile.php | |
region.user.profile.after | html region | Sections inside the profile card, under the numbers: badges (ctx: user, self) | app/views/profile.php |
region.user.profile.cards | list region | Cards under the profile card (list) | app/user.php |
region.user.profile.labels | html region | Profile card, after the group label: short labels such as a custom title (ctx: user, self) | app/views/profile.php |
region.user.profile.meta | inline region (loop, no DB) | Profile card details, after Joined / Seen / website: short items with an icon (ctx: user, self) | app/views/profile.php |
region.user.profile.stats | list region | Profile card numbers (list of label, value, url, sub; progress 0..1 draws a bar across the card) | app/user.php |
region.user.profile.tabs | list region | Profile page tabs (list) | app/user.php |
region.user.settings.tabs | list region | Settings page menu, a section list on phones (list): id => [label, group account | preferences |
regions.known | filter | Register extra regions for Admin → Widgets. | core/hook.php |
review.decided | event | Event: a moderator approved (status 1) or rejected (status 2) an item of the review queue (ctx: id, status, by). On approval the usual topic.after_save / post.after_save fire as well. | app/review.php |
review.held | event | Event: a topic or reply was put in the review queue (ctx: id, kind, topic_id, post_id, user_id, reason). | app/review.php |
review.hold | filter | Filter: why a new topic or reply waits in the review queue (value: the reason so far, an English text shown through t(), or empty; ctx: kind topic | reply, user, category, text). Return a reason to hold it. Administrators and moderators are never held. |
router.not_found | filter | No route matched (ctx: path). Return a URL (301) or [url, code] to redirect instead of the 404 page; return nothing to let it 404. Also the place to record misses. | core/router.php |
router.routes | filter | Filter: the route table (path => handler). Replace a core address (a portal at /); admin, sign-in, settings, setup and API addresses cannot be taken over. Admin → Plugins lists the takeovers. | core/router.php |
schema.install | event | After core tables are created/upgraded. | core/schema.php |
search.results | filter | app/search.php | |
security.csp | filter | Content Security Policy directives (name => list of sources): add the CDNs your plugin loads scripts, styles or fonts from. | core/security.php |
seo.sitemap | filter | GET /sitemap.xml: return the XML to serve instead of the built-in sitemap (an index, paged files); return nothing to keep the default. | app/api.php |
topic.access | filter | app/topic.php | |
topic.after_action | event | After pin/lock/move/restore (ctx: topic_id, action). | app/topic.php |
topic.after_delete | event | After a topic was soft-deleted. | app/moderation.php, app/topic.php |
topic.after_save | event | After a topic was created or edited (ctx: topic_id, post_id, new). A new topic that waits in the review queue fires it when a moderator approves it, so the topic is public by then. | app/topic.php |
topic.before_save | filter | New topic data (category_id, user_id, title, body, tags) before insert. | app/topic.php |
topic.can_reply | filter | app/topic.php | |
topic.category_auto | filter | Filter: whether a plugin will pick the category of a new topic (bool). True makes the category optional in the composer; return true only when topic.category_missing can actually answer (configured, within limits). | app/topic.php |
topic.category_missing | filter | Filter: a new topic arrived without a category (value 0; ctx: title, body, user). Return a category id to file it there (an AI pick, say); the writer must be allowed to post in it. Runs before the default category. | app/topic.php |
topic.posts | filter | Filter the posts of the current page (batch-loaded, attach extra data here). | app/topic.php |
topic.title | filter | Filter the escaped title html of a topic in lists and on the topic page (ctx: topic, where list | page); loop, no DB. |
topic.view | filter | Filter the topic row shown on the topic page (ctx: posts). | app/topic.php |
topic_list.query | filter | Filter the conditions of every topic list (front page, category, tag, unread): value ["where" => conditions on alias t, "params"]; append to both. Ctx: where, join. | app/home.php |
topic_list.rows | filter | Filter topic rows of any list (batch-loaded, attach extra data here). | app/home.php |
upgrade.after_apply | event | core/upgrade.php | |
upload.after_save | event | Event: a file was saved under uploads/ (ctx: kind attachment | avatar |
upload.before_save | filter | Filter: return a message to refuse an uploaded attachment, or change the file at ctx tmp in place (ctx: kind, user, tmp, name, ext, mime, size, is_image). | core/upload.php |
upload.url | filter | Filter: the address of an uploaded file (ctx: path), to serve it from a CDN or an object store. Runs for every avatar on a page: no database access. | core/upload.php |
user.after_delete | event | app/moderation.php | |
user.after_moderate | event | app/moderation.php, app/review.php | |
user.after_rename | event | After a username changed (ctx: user_id, old, new, by). Old profile URLs redirect automatically. | core/auth.php |
user.after_save | event | After the profile was saved. | core/render.php, app/user.php |
user.avatar_after | filter | HTML placed inside the avatar, over its lower corner (ctx: user, size in px); the wrapper is positioned, so use position:absolute. Runs inside lists: no database access. | core/render.php |
user.before_save | filter | Profile fields before saving. | app/user.php |
user.can | filter | Filter: whether the signed-in member may do something (bool; ctx: permission, user, group), after the group decided. Guests and admins never reach it. | core/auth.php |
user.email_changed | event | After a member changed their email address in Settings → Email, or restored the old one from the notice link (ctx: user_id, old, new, restored). | app/user.php |
user.level | filter | Filter: the level of a member as a number (ctx: user), read by user_level(). Answered by the plugin that keeps levels. Runs inside lists: no database access. | core/auth.php |
user.link_after | filter | HTML appended after every rendered username link (ctx: user, class; class is "profile-name" on the profile header). Runs inside lists: no database access. | core/render.php, app/views/card_newest.php, app/views/profile.php |
user.logout_everywhere | event | After every session of a user was invalidated (ctx: user_id). | core/auth.php |
user.prefs_save | filter | Preferences array before saving. | app/user.php |
user.profile_tab | filter | HTML for a custom profile tab (ctx: user, tab). | app/user.php |
user.settings_post | event | POST handler for a custom settings tab. | app/user.php |
user.settings_tab | filter | Extra HTML for a settings tab. | app/user.php |

