About canai-yolo
canai-yolo is an opt-in skill for managing PHP, CSS, and JS snippets stored in FluentSnippets through CanAI MCP tools. It focuses on snippet operations and deployment safeguards; it is intended only for explicitly requested snippet work.
Key features
- 01Lists and reads snippets
- 02Creates and updates snippets
- 03Publishes snippets with safeguards
- 04Replaces code with hash checks
- 05Checks stored-code hashes and cache status
- 06Documents CanAI language handling
Use cases
- 01Inspect site-specific snippets
- 02Create or update FluentSnippets code
- 03Publish a requested PHP snippet
- 04Verify a snippet write and cache status
MCP support
It uses the CanAI MCP server at `{site}/wp-json/mcp/wpcanai` with an API key. It describes tools for managing FluentSnippets, which appear only when the FluentSnippets plugin is active.
Install
Copy the command and run it in a terminal.
npx skills add https:/ / github.com/ usewp/ canai --skill canai-yoloAfter installing, type/canai-yoloin Claude Code to confirm it loaded.
Details
From the project READMECanAI YOLO — FluentSnippets MCP tools
High risk. Opt-in. This skill documents MCP tools that can permanently publish PHP on the WordPress site. Install and invoke it only when the user explicitly wants snippet work.
For templates, pages, i18n, media, settings, and Tailwind — use canai-mcp instead. Do not blend YOLO workflows into a normal /canai-mcp content session unless the user asked for this skill.
Transport is the same CanAI MCP server ({site}/wp-json/mcp/wpcanai) and API key as canai-mcp. Ability IDs use slashes; MCP tool names use hyphens.
CRITICAL — when to use this skill
Use canai-yolo only for:
- FluentSnippets (
easy-code-manager) — list/read/create/update/publish site PHP, CSS, or JS snippets via dedicated MCP tools.
There is no eval escape hatch. The
wpcanai/evalability was removed in plugin v1.59.0 — WP.org banseval()outright — andWPCANAI_ENABLE_EVALis no longer read. Every capability is a real ability; there is nothing to fall back on. If a workflow seems to need eval, the fix is to request the missing ability, not to improvise. An opt-in add-on plugin is planned (specs/2026-08-20-canai-eval-addon-design.md).
FluentSnippets (PHP/CSS/JS snippets)
Site-specific PHP, CSS and JS often lives in FluentSnippets rather than plugin code. Tools: wpcanai-list-snippets, wpcanai-get-snippet, wpcanai-create-snippet, wpcanai-update-snippet, wpcanai-replace-in-snippet, wpcanai-set-snippet-status. Their descriptions start with [fluent-snippets] / [fluent-snippets, filesystem] (plus cache when the site purges its page cache after writes) — see Tool labels in canai-mcp.
Key rules the schemas alone don't make obvious:
-
Tools only appear when FluentSnippets is active. If
easy-code-manageris inactive the six snippet tools are not registered at all — you won't see them in the tool list. -
Canonical form (plugin v1.80.0). Send PHP as the body without
<?php— a leading<?phpand blank lines are stripped on write (no longer rejected) and on read. Trailing newlines are kept; a trailing?>is removed.sha256/bytes/linesare computed over the code exactly asget-snippetreturns it, so hash that string, not your local file, when comparing. css/js must not be wrapped in their own<style>/<script>tag. PHP is syntax-checked and test-run once before it is saved: it must not redeclare a function or class that already exists (guard withfunction_exists/class_exists), print output,returnat the top level, or call something that only loads later (use a hook). -
Create defaults to a draft.
wpcanai-create-snippetwrites a draft unless you passstatus: "published"(v1.80.0), which syntax-checks PHP first and writes nothing on a syntax error. The responsestatusis read back — Fluent Snippets' ownauto_publishcan still promote a draft. -
Writes only succeed for allowlisted groups. An administrator lists agent-writable snippet groups at CanAI → AI Client → Guardrails → FluentSnippets group allowlist. An empty allowlist (the default) means all snippet writes are refused (
snippet_writes_disabled). A write to a non-allowlisted group returnssnippet_group_not_allowed. Prefer the groupAIunless the user specifies otherwise. Reads (list/get) are always allowed regardless of the allowlist. -
Updates are sparse. Send only the fields you want to change to
wpcanai-update-snippet; the server reads the current snippet, merges your changes over its full metadata, and writes it back. Omitting a field keeps its current value — you cannot wipegroup/priority/run_at/created_atby sending onlycode. -
run_atis type-specific. PHP →all|backend|frontend;php_content→shortcode|wp_head|wp_body_open|wp_footer|before_content|after_content; css →wp_head|admin_head|everywhere; js →wp_head|wp_footer|admin_head|admin_footer. An invalid pairing is rejected before FluentSnippets is called (invalid_snippet_type/invalid_run_at). -
Upstream quirks (not fixed here). CSS
everywheredoes not actually load in admin (a plugin typo,everywehere), and PHPfrontendis not enforced (it runs everywhere) — both values are still accepted because they are what the FluentSnippets UI offers. Preferall/backend/wp_headwhen unsure. -
Reactivation. If a snippet fatally errored, FluentSnippets auto-disables it and
has_erroris true. An update to it is refused (snippet_has_error) unless you pass"reactivate": true, which clears the error and applies the update — so a routine edit can't silently re-arm known-broken code. -
Errors explain themselves (v1.80.0). A save Fluent Snippets refuses comes back as
"{title} on line {line}: {reason} Fix: {fix}"in the error message — read the reason and the fix before retrying; don't resend the same code.
Moving a snippet to a different group (via wpcanai-update-snippet's group) requires both the current group and the destination group to be allowlisted.
Deploy loop (plugin v1.80.0)
Edit a live snippet without resending it and confirm it without a browser:
- Replace —
wpcanai-replace-in-snippet { "file_name", "replacements": [...], "require_all": true, "expected_sha256"?: "<sha256 from your last read>" }.require_allmakes a miss write nothing and name the nearest match;expected_sha256refuses the write withhash_mismatch(409) if someone changed the snippet since you read it. Usedry_run: truefirst when unsure of the anchors. Whole-body rewrites still go throughwpcanai-update-snippet(sameexpected_sha256). - Check — the response carries
sha256,bytesandlinesof the code as stored, read back after the write. Comparesha256with the hash of the code you expect (over the canonical form above). Later, or from another session,wpcanai-get-snippet { "file_name", "fields": ["hash"] }returns the same fields without the code. - Cache — the response's
purgedsays whether a page-cache purge is pending:{ "scope": "all", "plugin": "…" }means the whole site is purged at the end of the request (a snippet runs on every page);nullmeans nothing was queued. Callwpcanai-purge-cache(incanai-mcp) only whenpurgedisnullwhile a cache plugin is active (purge after writes is off), or for a URL outside the write.
Writing custom REST routes in a snippet
Agents often implement browser → WordPress relays as a FluentSnippets PHP route plus _canai_js fetch. Two rules the schemas alone don't make obvious:
-
Never send a custom nonce under
X-WP-Nonce(or request param_wpnonce). WordPress core'srest_cookie_check_errors()intercepts that exact header/param on every REST request site-wide and validates it against the built-in'wp_rest'action — before your route'spermission_callbackruns. A custom-action nonce under that name is always rejected withrest_cookie_invalid_nonce, masking your own permission logic. Use any other header name for a custom nonce action (e.g.X-<Your-App>-Nonce). -
Surface HTTP status / error code to the client (console log or distinct on-page states) even when user-facing copy stays generic. A rate-limit
429, a nonce403, and a real500are different failures; collapsing them into one message makes production incidents impossible to triage.
The visitor's language in a snippet (plugin v1.99.0)
On a site with CanAI native translation (wpcanai-i18n-get-settings → enabled: true), a snippet that writes visitor-facing text (SEO meta, form labels and messages, emails, redirects after a form) must follow the page's language. Ask CanAI, not WordPress. get_locale() / determine_locale() follow the language only on languages whose owner ticked Show WordPress and plugin text in this language (plugin v1.100.0); otherwise they stay the Site Language on /ms/ pages. $_SERVER['REQUEST_URI'] carries the /ms prefix only from the parse_request action onwards (v1.100.0); before that, and always on older plugins, CanAI removes it. Ask CanAI:
| Function | Returns |
|---|---|
wpcanai_current_language() | The language slug ('en', 'ms'), or null where it isn't known: admin, AJAX, REST (however it is reached), cron, WP-CLI, WordPress system paths such as wp-login.php and entry points other than index.php (v1.100.0), before setup_theme, a site with one language. |
wpcanai_language_from_url( string $url ) | The language a URL of this site renders in (?lang=, then the prefix, else the default); null for another site, a system path (/wp-admin/…, /wp-json/…) or one language. |
wpcanai_translate( string $source, array $vars = [], ?string $lang = null ) | The string from CanAI → Translations, rendered exactly as Twig t() (inline tags, {placeholders}, stale translations never shown); the source for the default or an unknown language. $lang null = wpcanai_current_language(). |
do_action( 'wpcanai_language_detected', $lang, $request_uri ) (v1.100.0) | An action, not a function: it fires once per front-end request, at setup_theme priority 1, as soon as the language is known. $request_uri is the visitor's URI, raw and slashed: wp_unslash() it, and escape it before printing. |
// This request's language: the page's, else the form's hidden field, else the Referer.
if ( ! function_exists( 'mysite_request_lang' ) ) {
function mysite_request_lang(): string {
if ( ! function_exists( 'wpcanai_current_language' ) ) {
return 'en'; // CanAI older than 1.99.0: the default language
}
$lang = wpcanai_current_language();
if ( null === $lang && isset( $_POST['mysite_lang'] ) ) {
$lang = sanitize_key( wp_unslash( $_POST['mysite_lang'] ) ); // an unknown slug translates to the source
}
return $lang ?? wpcanai_language_from_url( (string) wp_get_referer() ) ?? 'en';
}
}
// In the code that prints the form (a shortcode, a form plugin's markup filter), never at the top level:
printf( '<input type="hidden" name="mysite_lang" value="%s">', esc_attr( mysite_request_lang() ) );
// Wherever visitor-facing text is built, on the page or in the form handler:
$subject = wpcanai_translate( 'Your download is ready', array(), mysite_request_lang() );
Rules:
- Read the language inside a hook (
template_redirect,wp_head, a form plugin's filter), never at the snippet's top level: the functions exist as soon as CanAI loads, but the language is only known fromsetup_theme, and the save-time test run has no page at all (it returnsnull). Hookwpcanai_language_detectedwhen the code must act beforetemplate_redirect. - Never derive the language yourself. No parsing
REQUEST_URI(the prefix is there only after routing, and?lang=and system paths follow CanAI's rules), noget_locale()(it follows the language only where the owner turned that on), nowpcanai_langcookie (it keeps the last translated page the visitor saw, not the page they are on), and no CanAI internals (MenuLocations::language_manager(),new StringStore(),remove_actiononLanguageManagermethods) — those can change in any release. - Forms posted to
admin-ajax.php/ REST have no page language (wpcanai_current_language()isnull). Print the language into the form when it renders (a hidden field fromwpcanai_current_language()) and read that first;wpcanai_language_from_url( (string) wp_get_referer() )is the fallback, because browsers don't always send a Referer. Store the language on the entry if a later step (an email, a queued job) needs it — a cron run has no request to ask. - Escape the result where you print it. A plain source comes back as plain text (like
__()); one with inline tags comes back as HTML with its text already escaped. - Strings only PHP uses aren't listed on the Strings tab or in
wpcanai-i18n-list-strings(both listt()sources from templates and menus). Store their translations withwpcanai-i18n-set-translations(incanai-mcp), keyed by the exact source the snippet passes, and give the user the source → translation table. - Cookie redirect. A logged-out visitor whose language cookie names a translation is 302'd from an unprefixed CanAI page to that translation. If the owner wants every URL to serve its own language:
add_filter( 'wpcanai_redirect_to_cookie_language', '__return_false' );(args$redirect, $lang, $target). Don'tremove_actionCanAI's hook.
wpcanai-list-snippets
- Args:
{ "status"?: string, "type"?: string, "group"?: string, "include_hash"?: bool }— all optional filters (status:published|draft;type:PHP|php_content|css|js;group: group name). (v1.80.0)include_hash: trueaddssha256,bytes,linesper snippet (reads every file, so opt-in). Only appears when FluentSnippets is active. - Returns:
{ "snippets": [{ "file_name", "name", "type", "status", "run_at", "priority", "group", "tags", "description", "created_at", "updated_at", "has_error", "error_message", "writable" }], "groups": string[], "writable_groups": string[] }—writableis per-snippet (its group is on the allowlist);writable_groupsis the current allowlist so you know what you may touch before attempting a write. Reads are unrestricted.
wpcanai-get-snippet
- Args:
{ "file_name": string, "fields"?: ["meta"|"code"|"hash"] }— the snippet's file name (its identifier, e.g.3-my-snippet.php);fieldsdefaults to all three. - Returns: full metadata + the
codebody + error state +writable(same fields as alist-snippetsrow pluscodeandcondition) + (v1.80.0)sha256,bytes,lines.fields: ["hash"]returns onlyfile_name,status,group,updated_at,sha256,bytes,lines— use it to confirm a write without pulling the code. Errorsnippet_not_foundfor an unknown file.
wpcanai-create-snippet
- Args:
{ "name": string, "type": string, "run_at": string, "group": string, "code": string, "description"?: string, "tags"?: string, "priority"?: int, "status"?: "draft"|"published" }— all ofname/type/run_at/group/coderequired. PHPcodegoes without<?php(a leading one is stripped).type/run_atare validated against the type table.groupmust be on the writable allowlist.statusdefaults todraft;publishedlints PHP first. - Returns:
{ "success": true, "file_name": string, "status": string, "group": string, "note": string, "sha256", "bytes", "lines", "purged" }— the file name is auto-generated (you can't choose it) and is what every later call keys on.statusis read back; a draft goes live later withwpcanai-set-snippet-status.
wpcanai-update-snippet
- Args:
{ "file_name": string, "code"?: string, "name"?: string, "description"?: string, "tags"?: string, "group"?: string, "run_at"?: string, "priority"?: int, "reactivate"?: bool, "expected_sha256"?: string }—file_namerequired; send only fields to change (sparse — the server merges over current metadata). The current group (and destinationgroup, if moving) must be allowlisted. An errored snippet needsreactivate: true. (v1.80.0)expected_sha256refuses a stale write withhash_mismatch(409, both hashes in the message, nothing written). - Returns:
{ "success": true, "file_name": string, "changed_fields": string[], "sha256", "bytes", "lines", "purged" }— hash fields read back after the write. - Published PHP code updates: Updating
codeon a published PHP snippet used to fatal withCannot redeclare function(FluentSnippets validates in the same request where the live snippet is already loaded). CanAI handles this transparently. If an older plugin build still returns that error, unpublish → update → republish viawpcanai-set-snippet-status.
wpcanai-replace-in-snippet (plugin v1.80.0)
- Args:
{ "file_name": string, "replacements": [{ "from": string, "to": string }], "require_all"?: bool, "ignore_whitespace"?: bool, "regex"?: bool, "scope"?: { "text": string, "occurrence"?: int }, "dry_run"?: bool, "expected_sha256"?: string }— thewpcanai-replace-in-metacontract (seecanai-mcp) applied to a snippet's code: samerequire_allmiss diagnostics in thereplace_no_matchmessage, sameignore_whitespace/regex/scope/dry_run. The write goes throughupdate-snippet(published PHP is linted first) and the same group allowlist. - Returns:
{ "success", "file_name", "total", "dry_run", "replacements": [{ "from", "to", "count", "suggestion"? }], "sha256", "bytes", "lines", "purged"? }— ondry_runthe hash is of the current code and there is nopurged.
wpcanai-set-snippet-status
- Args:
{ "file_name": string, "status": "published"|"draft" }— the snippet's group must be allowlisted. - Returns:
{ "success": true, "file_name": string, "status": string, "sha256", "bytes", "lines", "purged" }. Fires both FluentSnippets lifecycle actions so the index cache rebuilds and a published snippet actually runs.
Action router (quick)
| Goal | Tools |
|---|---|
| List / read FluentSnippets snippets | wpcanai-list-snippets / wpcanai-get-snippet |
| Create / update a snippet | wpcanai-create-snippet (status optional) / wpcanai-update-snippet |
| Edit part of a snippet (deploy loop) | wpcanai-replace-in-snippet → check sha256 → purged |
| Confirm what is stored, no code | wpcanai-get-snippet { "fields": ["hash"] } / wpcanai-list-snippets { "include_hash": true } |
| Publish / unpublish a snippet | wpcanai-set-snippet-status |
| Visitor's language / translated text in a snippet (v1.99.0) | wpcanai_current_language(), wpcanai_language_from_url(), wpcanai_translate() in the PHP — see The visitor's language in a snippet; translations via wpcanai-i18n-set-translations (canai-mcp) |
Install
npx skills add usewp/canai --skill canai-yolo
Pair with canai-mcp for content work on the same MCP endpoint.