Hard stop internals: wp_ai_client_prevent_prompt
Technical reference for how AI Spend Governance blocks AI calls using WordPress core's wp_ai_client_prevent_prompt filter — pipeline position, error surfaces, evaluation order, and attribution edge cases.
Updated
This page documents exactly how the hard stop works at the code level. It’s written for developers evaluating the product, plugin authors wondering what their plugin experiences when blocked, and anyone building on wp_ai_client_prevent_prompt themselves.
The core filter
WordPress 7.0’s AI Client consults one filter before dispatching any prompt to a provider:
apply_filters( 'wp_ai_client_prevent_prompt', false, $builder );
It fires for both capability checks (is_supported_*()) and generation calls. Returning true stops the pipeline inside WordPress — no HTTP request is built, nothing reaches the provider, nothing is billed. The $builder argument is a clone, so callbacks can inspect the pending prompt but not mutate the real one.
Verified behavior (live-provider QA, evidence on request): calls blocked by this filter never appear in the provider’s own logs — blocking happens strictly pre-dispatch.
What a blocked plugin experiences
| Call type | While blocked |
|---|---|
is_supported_*() capability check | Returns false — AI reads as “not available,” same as no provider configured |
Generation (wp_ai_client_prompt() and fluent equivalents) | WP_Error with code prompt_prevented |
Plugins that check capability first hide their AI UI cleanly. Plugins that don’t still receive a standard error they must already handle (it’s the same surface as an unconfigured provider). No exceptions or fatals are introduced — by core’s design or ours.
Governance’s evaluation order
Our callback runs at priority 10 and respects prior blocks (if an earlier filter already returned true, we never overrule it to false). For each call:
- License check — fails open: on any network error, cached state is used and enforcement continues. An outage on our side can never disable your budgets.
- Kill switch — if enabled, block immediately (scope
kill_switch), regardless of budgets. - Self/attribution resolution — identify the calling plugin via call-stack inspection, skipping Governance’s own frames (see edge cases below). Calls attributed to Governance itself are never blocked.
- Threshold alerts — evaluated on every call, whether or not hard stop is enabled, so alerts-only mode works.
- Hard stop — if enabled: block when month-to-date estimated spend ≥ budget × (hard-stop % ÷ 100), checking the sitewide budget first, then the calling plugin’s budget (its own row, else the default per-plugin budget).
Every blocked call is recorded into the Monitor’s table with status = blocked and the scope that triggered it (kill_switch, budget:site, or budget:plugin), so the audit trail shows what was refused and why. Spend lookups are cached (60s) to keep the hot path cheap.
Attribution edge cases
Attribution walks debug_backtrace() to the first file inside a plugin or theme directory (the Query Monitor technique, implemented in the Monitor’s Aismon_Attribution class):
- Cron: calls made from WP-Cron attribute to the plugin whose callback is running — correct for budgeting purposes.
- Custom code outside plugins/themes (e.g., WP-CLI scripts, code in
wp-contentroot): attributes ascoreand is governed by the sitewide budget only. - Self-exclusion: an enforcement callback sits in the call stack of every call it inspects. Naive attribution resolves to the enforcement plugin itself; ours skips its own directory frames (
Aismon_Attribution::resolve( $skip_dirs )). If you build on this filter yourself, this is the bug you’ll hit first — we did.
What this layer cannot govern
Plugins that ship their own API key and call providers directly bypass the AI Client — and therefore this filter — entirely. No WordPress-layer enforcement can reach them. The free Monitor’s coverage test (trigger each AI feature once; covered plugins appear in the dashboard) identifies them in about a minute.
Filter composition caveat
wp_ai_client_prevent_prompt follows the standard WordPress filter contract: a later callback at higher priority could return false after we return true. Nothing in the ecosystem currently does this, and doing so would be a deliberate act, but there is no “final say” mechanism in core filters — worth knowing if you audit your stack.
Related
- Alerts and thresholds reference — when emails send, throttling semantics
- Budget rollout checklist — verifying the hard stop on staging
- Blog: How the WordPress 7.0 AI hard stop actually works — the narrative version, with a DIY kill-switch snippet
Question about this page?
This form tags your question with the product, docs page, and category so support can triage it quickly.