This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
FluentAuth (fluent-security) is a WordPress security plugin providing login security, audit logging, magic login, 2FA, social login (Google/GitHub/Facebook), login customization, and system email management.
Small businesses. Usually one administrator. Often the owner is the only user.
This is not enterprise software and must not be built like it. Keep things stupid simple. When a design decision is open, take the simpler branch — even when the more thorough one is more defensible in the abstract.
What that rules out, concretely:
- Do not intercept ordinary traffic to enforce a policy. No blanket refusals of REST calls, admin-ajax calls, or page loads. A guardrail that 403s normal site activity costs a real site owner more than the thing it prevents, and the symptom never points back at this plugin. Enforce at the door — during login — where the answer is unambiguous and nothing else can break.
- Prefer telling people over stopping them. An admin notice with a link to the fix beats a redirect, a modal, or a locked screen. If a setting takes effect at next login, say so and let them carry on.
- One way to do a thing, not three. No layered fallbacks or backstops "in case the first one is bypassed".
- Do not add a setting, filter, or allowlist to soften a guardrail — remove the guardrail instead.
The load-bearing example: 2FA enforcement used to redirect users out of wp-admin and refuse
admin-ajax and REST wholesale for anyone who owed a second factor. It broke front-end form
submissions and add-to-cart for logged-in members, and one missing allowlist entry locked a
3.0.1 user out of their own site with no route back in. All of it was replaced by a single
admin notice (TwoFaReminderHandler), because the requirement is already applied during
login by EnrollmentTwoFaMethod, before a cookie exists. Less code, no breakage, same
protection where it counts.
# PHP tests
composer test # Run PHPUnit suite
./tests/vendor/bin/phpunit tests/Unit/HelperTest.php # Single test file
./tests/vendor/bin/phpunit --filter "testMethodName" # Single test method
composer test-coverage # HTML coverage report
# Static analysis
composer phpstan # PHPStan level 5 (outputs to phpstan-errors.md)
# Frontend build (Laravel Mix — no npm scripts defined, use npx directly)
npx mix # Development build
npx mix --production # Production build
npx mix watch # Watch mode
# Release build
sh build.sh --loco --node-build # Full build for WP.org distributionEntry point: fluent-security.php — defines constants, registers a custom PSR-4-style SPL autoloader for the FluentAuth\ namespace (maps FluentAuth\App\* to app/*), then loads hooks and REST routes.
FluentAuth\App\Http\Controllers\— REST API controllers (5 controllers: Settings, Logs, SocialAuthApi, SystemEmails, SecurityScan)FluentAuth\App\Http\routes.php— All REST endpoint definitions (namespace:/wp-json/fluent-auth/)FluentAuth\App\Services\— Business logic (AuthService, social auth services, SystemEmailService)FluentAuth\App\Services\DB\— Custom fluent query builder wrapping wpdb; accessed viaflsDb()globalFluentAuth\App\Hooks\Handlers\— 12 WordPress hook handlers, each with aregister()methodFluentAuth\App\Hooks\hooks.php— Instantiates and registers all handlersFluentAuth\App\Helpers\— Helper (settings/utilities), Arr (array ops), Activator (DB migrations), BrowserDetectionFluentAuth\App\Views\— PHP templates (magic login views, email templates)src/admin/— Vue 3 admin SPA (components: Dashboard, Settings, Logs, SocialAuthSettings, AuthCustomizer, ServerMode, SecurityScan, CustomWpEmails)src/admin/styles/— all admin CSS. Vue SFCs carry no<style>blocks; every rule lives in a partial here, listed in load order insrc/admin/app.scss(which documents the layering). A rule belongs with the screen that renders it, and moves up to_ui.scssonly once a second screen needs it.src/public/— Public-facing JS (magic_url.js, one_tap.js, login_helper.js, login_customizer.scss)
- Database access: Always use
flsDb()->table('fls_auth_logs')->where(...)->get()(Laravel-style query builder). Two custom tables:fls_auth_logs,fls_login_hashes. - REST API: Routes defined in
app/Http/routes.phpusing the customRouterclass with method chaining. Default permission:manage_options, overridable viafluent_auth/app_permissionfilter. - Error handling: Return
\WP_Errorobjects from controllers/services for API errors. - Settings: Stored in
wp_optionsunder key__fls_auth_settings. Retrieved/managed viaHelper::getAuthSettings(). - Hook handlers: Each handler in
app/Hooks/Handlers/is a class with aregister()method that hooks into WordPress actions/filters. - Frontend: Vue 3 + Element Plus + Vue Router, built with Laravel Mix. Admin app bootstraps from
src/admin/app.jsusingwindow.fluentAuthAdminfor server-side data. REST calls use mixin methods$get,$post,$put,$del. Vue i18n via$t()mixin method. - i18n text domain:
'fluent-security'
Tests run standalone without WordPress via comprehensive mocks in tests/bootstrap.php (mocks WP functions, database, REST classes). Test namespace: FluentAuth\Tests\ (PSR-4 in autoload-dev). Unit tests in tests/Unit/, integration tests in tests/Integration/. Mock WordPress classes (WP_Error, WP_REST_Request, WP_User) are in tests/Unit/MockClasses.php.
- PHP: PascalCase classes, camelCase methods/variables, UPPER_SNAKE_CASE constants. Follow WordPress coding standards. Sanitize input (
sanitize_text_field,sanitize_url), escape output (esc_html,esc_url). - Vue/JS: PascalCase component files, kebab-case in templates. ES6+, async/await. SCSS with BEM naming (
.fframe_app,&__body).<script>before<template>before<style>in SFCs. - Translations: Use
__('text', 'fluent-security')in PHP and$t('text')in Vue. Never split a sentence across several$t()calls or join a$t()result to a value; use placeholders ($t('Try again in %s minutes.', minutes),$_n('%s file changed', '%s files changed', n)), and$_x('Open', 'Verb')when one English word needs different translations in different places. Vue strings reach translators through the generatedapp/Services/TransStrings.php: after adding or changing a$t()/$_n()/$_x()string, runnpm run translate. Put a<!-- translators: ... -->or// translators: ...comment on the line directly above any string with placeholders;npm run translatefails on a placeholder string without one, on em/en dashes in UI text, and on keys with leading or trailing whitespace. Long paragraphs go inreserved18n.jsonunder a__snake_case_key__and the template calls$t('__snake_case_key__'). Write for a small-business owner: short sentences, one idea each, everyday words. Rewording a string that is already clear costs its translation in every locale, so leave good strings alone. Seedocs/js-translations.md. - Commits: Run
composer phpstanbefore committing.