Use any AI provider (via WordPress 7.0 Connectors) to inspect and modify WordPress files directly from WP-Admin — within strict guardrails. No shell, no SSH.
Inspired by Matt Mullenweg to build this. Props to Cem Ünalan for the original idea of a plugin-generating plugin.
Try it in WordPress Playground →
The page is a single full-screen chat. There is no built-in file browser or editor — file references in the AI's replies are clickable and adapt to what your site actually allows:
- If WordPress core's
plugin-editor.php/theme-editor.phpare available for the file type, the AI links straight to them so you can hand-edit in a new tab. - Otherwise (e.g. on managed hosts that set
DISALLOW_FILE_EDITorDISALLOW_FILE_MODS, or for files outside plugin/theme dirs), the link expands a read-only inline viewer below the message and the AI is told to point you at SFTP / WP-CLI / your local IDE for edits.
A fresh chat greets you with a row of suggestion chips spanning the assistant's capabilities (one prompt each from file-ops, database, plugins, and PHP). Click a chip to drop the prompt into the input. The chips disappear once you send your first message, and a different set is rolled the next time you start a new chat.
When the AI proposes a file change a panel slides in above the chat showing a unified diff against the live on-disk content (so the diff is always current, not whatever you happen to have open in another tab). The panel disappears as soon as you approve, reject, or move on.
The AI assistant always has access to wp-content/plugins/ and wp-content/themes/.
Nothing else is ever accessible (not core, not wp-config.php, not .htaccess).
Optionally create wp-content/uploads/ai-edits/ as a scratch directory.
If Jetpack is installed and connected, the plugin enriches the system prompt with site-specific context (stats, top posts, referrers, search terms, active modules, plan tier, speed scores, social connections, security data) so suggestions are tailored to the site rather than generic. Without Jetpack, a dismissable banner appears at the top of the chat panel pointing to install/connect; everything else still works.
You can also launch the assistant from anywhere in WP-Admin via the command palette (Cmd/Ctrl+K → "Interact with AI").
Generate an API token under Advanced settings in the Haydi sidebar (Tools → Haydi) to connect local AI tools directly to your site.
Claude Code (and any MCP-compatible tool): add to ~/.claude/claude_code_config.json:
{
"mcpServers": {
"haydi": {
"url": "https://yoursite.com/wp-json/haydi/v1/mcp",
"headers": { "Authorization": "Bearer <your-token>" }
}
}
}Claude Code can then use all Haydi tools (haydi_list_files, haydi_list_posts, haydi_run_query, etc.) as MCP tools — no browser needed. Write tools (haydi_write_file, haydi_edit_file, haydi_run_php, etc.) are available when the corresponding extensions are installed (see Extensions below).
The same token also works against the REST API directly: GET /wp-json/haydi/v1/files, GET /wp-json/haydi/v1/file, etc. Write operations execute immediately when authenticated via token (the token is the approval gate) and require the relevant extension to be installed.
GET /wp-json/haydi/v1/status returns site info plus an allowed_roots array — the same paths exposed by the haydi_get_allowed_roots MCP tool — so clients can discover valid write targets without guessing.
Browser PHP (this plugin) AI Connector (WP Connectors)
│ │ │
│── user message ────────────►│ │
│ │── messages + tools ─────────►│
│ │◄── tool_use (fetch/list/read)│
│ │ [execute locally] │
│ │── tool_result ──────────────►│
│ │ (loop until done or approval needed)
│ │◄── tool_use (write / query / install …)
│ │ [NOT executed yet] │
│◄── pending proposal ────────│ │
│ [diff, SQL, PHP, … + buttons] │
│── "Apply" / "Execute" ─────►│ │
│ │ [backup + write / execute] │
│◄── success ─────────────────│ │
fetch_url, list_files, read_file, search_files, list_plugins, list_posts, list_users, and list_options run automatically.
Everything that mutates the filesystem, database, or plugin state always pauses for human approval.
| Tool | Auto? | What it does |
|---|---|---|
fetch_url(url) |
Yes | Fetches a public URL, strips HTML, truncates at 100 KB. Private IPs blocked. |
get_allowed_roots() |
Yes | Returns the list of absolute directory paths Haydi is allowed to read/write. Call this before writing files to pick a valid target path. |
list_files(path) |
Yes | Lists files/dirs inside an allowed root. |
read_file(path) |
Yes | Reads a file (max 512 KB). |
search_files(query, path, mode, extensions, max_results) |
Yes | Searches allowed file contents using PHP (no shell grep). Empty optional fields use safe defaults. |
list_plugins() |
Yes | Lists all installed plugins with name, version, file path, and active status. |
list_posts(status?, type?, limit?) |
Yes | Lists posts/pages with ID, title, status, type, date, and content. Defaults: any status, post+page types, 50 most recently modified. |
list_users(role?, limit?) |
Yes | Lists users with ID, login, email, display name, and roles. Defaults: all roles, 50 most recently registered. |
list_options(search?) |
Yes | Lists WordPress options. Without a search term returns autoloaded options; with a search term filters option_name by substring. Capped at 100 rows. |
list_backups(path?) |
Yes | Lists backup files created by the plugin. Optionally filter by original file path. |
install_plugin(slug, reason) |
No | Proposes installing a plugin from WordPress.org by slug. |
activate_plugin(plugin, reason) |
No | Proposes activating an installed plugin by file path. |
deactivate_plugin(plugin, reason) |
No | Proposes deactivating an active plugin. |
| Tool | Extension | Auto? | What it does |
|---|---|---|---|
write_file(path, content, reason) |
haydi-files.php |
No | Proposes a file write. You see the full content before applying. |
edit(filePath, oldString, newString, replaceAll, reason) |
haydi-files.php |
No | Proposes an exact-string edit to an existing file. oldString must match once unless replaceAll is true. |
delete_file(path, reason) |
haydi-files.php |
No | Proposes deleting a file. A backup is created automatically. |
move_file(src, dest, reason) |
haydi-files.php |
No | Proposes moving/renaming a file. Source is backed up first. |
copy_file(src, dest, reason) |
haydi-files.php |
No | Proposes copying a file. Destination is backed up if it already exists. |
delete_dir(path, reason) |
haydi-files.php |
No | Proposes recursively deleting a directory. All files are backed up first. Root directories cannot be deleted. |
restore_backup(backup_file, original_path, reason) |
haydi-files.php |
No | Proposes restoring a file from a specific backup. Use list_backups first to find the backup filename. A new backup of the current file is created before restoring. |
run_query(sql, reason) |
haydi-db.php |
No | Proposes SQL via $wpdb. You see the full query before it runs. |
run_php(code, reason) |
haydi-php.php |
No | Proposes executing a PHP snippet in the WordPress context. Output is captured and returned. |
The core plugin is read-only (plus plugin management) so it can be distributed through WordPress.org. Write and execute capabilities are provided by extension files that you drop into wp-content/plugins/haydi/extensions/ via SFTP — no activation step needed, Haydi auto-loads any .php file placed there.
Option A — Download the full bundle from GitHub releases (haydi-full-extensions.zip): all three extension files are pre-bundled alongside the core plugin.
Option B — Install extensions individually: download haydi-files.php, haydi-db.php, and/or haydi-php.php from the GitHub releases page and upload only the ones you need.
See extensions/README.md (inside the plugin folder) for details.
Fetch https://example.com/pricing and create a WordPress plugin that renders
the same pricing table as a [pricing_table] shortcode.
The AI fetches the URL, reads the markup, proposes plugin files one at a time. You review each diff and click Apply.
Open a file in the browser panel, then describe the change:
Add a $limit parameter (default 10) to list_posts() and pass it to WP_Query.
The AI reads the file, proposes the updated version, you diff and apply.
Rename class-old-name.php to class-new-name.php and update the class name inside it.
The AI reads the file, proposes a move_file to rename it, then a write_file with the updated class name. Each step requires your approval.
Create a table called wmp_feedback (id, post_id, rating, comment, created_at)
using the WordPress table prefix, then insert a test row.
Each CREATE, INSERT, or SELECT is shown to you before it runs.
Results are fed back to the AI so it can continue automatically.
Install and activate WooCommerce, then configure it for selling digital products.
The AI checks what is already installed (list_plugins), proposes installing WooCommerce, activating it, and then uses run_php or run_query to apply configuration — one approved step at a time.
My functions.php is broken after the last edit — restore it to the previous version.
The AI calls list_backups to find available backups for that file, presents what it found, and proposes restore_backup. A new backup of the current (broken) file is created first so the restore is itself reversible.
| Control | Detail |
|---|---|
| Auth | manage_options + nonce on every request |
| Path isolation | realpath() + allowlist on every read/write; nothing above ABSPATH |
| Extension allowlist | .php .css .js .json .txt .md .html only |
| File size cap | 512 KB reads/writes |
| Dotfiles | Always skipped |
| Backup | Timestamped .bak created before any write, delete, or move |
| Backup dir | Protected with .htaccess Deny from all; filenames carry a random token so URLs are not enumerable on web servers that ignore .htaccess (Nginx, LiteSpeed) |
| Post-mutation health check | After every approved write/delete/move/copy/activate/run_query/run_php, the plugin fires a loopback request to a dedicated health endpoint pinned to 127.0.0.1. If the site is no longer responding, file ops auto-restore from backup and activate_plugin auto-deactivates. delete_dir, run_query, and run_php are detect-only (no automatic revert) and surface a recovery-mode message instead. PHP writes are also pre-validated with token_get_all() so syntax errors are caught before the file ever hits disk. |
| Writes/deletes/moves/queries/installs | Require a human click by default. A session-only Auto-accept toggle can opt every proposal in until the page is reloaded. |
| Audit log | All operations logged to wp_options |
| API key | Managed via Settings → Connectors; never sent to browser |
fetch_url SSRF |
Scheme must be http/https; every A and AAAA record is resolved and validated against private/loopback/link-local ranges; cURL is pinned to those IPs (defeats DNS rebinding); redirects are disabled (would re-resolve DNS); response body capped at 200 KB |
run_query |
Full SQL shown before execution; SELECT results capped at 200 rows |
install_plugin |
Slug validated against ^[a-z0-9][a-z0-9-]*$; downloads only from WordPress.org API |
run_php |
Full code shown before execution; output captured and returned |
| Remote Access tokens | SHA-256 hash stored; plaintext shown once at generation; revocable from Advanced settings in the sidebar; Bearer token auth on all REST / MCP routes |
| REST API write ops | Execute immediately when token-authenticated; same guard, health-check, backup, and audit-log machinery as browser-initiated changes |