phpClaw for Drupal
The Drupal adapter is a module. It adds a chat page and admin pages under Configuration, two Drush commands, two REST routes, three permissions, and up to 15 Drupal tools.
Install
Section titled “Install”| Requirement | Version |
|---|---|
| PHP | 8.1 or later |
| Drupal | 10 or 11 |
composer require phpclaw/phpclaw-drupaldrush en phpclawEnabling the module creates phpclaw_conversations, phpclaw_messages and phpclaw_memory from
hook_schema(). After updating the module, run drush updb for pending updates.
Open Configuration → phpClaw AI Agent → Settings, choose a provider, enter the API key and save. Test Connection checks it.

To update the module later:
composer update phpclaw/phpclaw-drupal --with-dependenciesdrush updb -ydrush crSettings
Section titled “Settings”The settings live in the phpclaw.settings config object. The Settings form has:
| Field | Notes |
|---|---|
| Provider, Model, API Key | the provider to use; the key is not needed for Ollama |
| Base URL | for the Custom provider; https://, or http:// on localhost, 127.0.0.1 or ::1 |
| System Prompt | added to every request |
| Store Messages | on by default |
| Max Iterations | the agent loop limit; 0 or empty uses 20 |
| Remote Skill URLs | comma-separated HTTPS URLs of skill files; the Guide page shows which loaded |
| Cloud Key, Cloud Signing Secret, Cloud Disable | configure phpClaw Cloud; cloud is used only while Store Messages is on |
Drush writes the same config:
drush config:set phpclaw.settings provider anthropicdrush config:set phpclaw.settings api_key sk-ant-...These keys have no form field. Edit them with drush config:edit phpclaw.settings:
| Key | Default | Notes |
|---|---|---|
shell_allowlist | not set | empty uses core’s default allowlist |
tool_deny | [] | tool names or groups to remove |
guards | [] | guard class entries |
hooks | [] | event, handler and priority entries; the handler must be callable |
skills | [] | skill definitions |
api_auth_providers | [basic_auth, cookie] | authentication providers accepted by the REST routes |
With Store Messages off, the agent stores no message content. See Memory.
Permissions
Section titled “Permissions”| Permission | Allows |
|---|---|
use phpclaw chat | the Chat, Analytics, Guide and About pages, the REST routes, and every Drupal tool |
manage all phpclaw conversations | seeing every user’s conversations and counts on Chat and Analytics |
administer phpclaw | the Settings page and Test Connection |
All three are marked restricted.
A tool call without the permission returns a FORBIDDEN error. Tools run from Drush, including the
MCP server, skip the check.
The phpClaw menu item is attached to the Chat route, so a role with use phpclaw chat but not
administer phpclaw still sees the menu. To reach it from the admin menu, the role also needs
Drupal’s own Use the administration pages and View the administration theme permissions.
Where each tool is available
Section titled “Where each tool is available”The adapter builds two agents with different tool sets:
| Where | Tools |
|---|---|
| Chat page and REST API | the Drupal tools only |
Drush phpclaw:run, the phpclaw.agent service, and the MCP server | the Drupal tools plus core’s shell_exec, http_request, file_read, file_write, file_edit, code_search and project_info |
So the chat page and the REST API cannot run shell commands, make HTTP requests or touch files.
The Drupal tools
Section titled “The Drupal tools”Eight are always registered:
| Tool | What it reads |
|---|---|
drupal_entity | nodes, users and taxonomy terms, with filters and field selection; each row passes Drupal’s own view access check, and the response reports total_before_access_filter |
drupal_config | a value from an allowlisted set of core configuration objects, by name and optional key |
db_query | one read-only SQL SELECT |
drupal_modules | installed modules with status, version and package |
drupal_cron | last cron run, queue sizes and queue workers |
drupal_roles | roles with their permissions and user counts |
drupal_path_aliases | path aliases: list them, find the alias for a path, or search alias text |
drupal_cache | cache bins with approximate row counts; it never flushes |
Seven are registered only when the module they read is installed:
| Tool | Needs module | What it reads |
|---|---|---|
read_log | dblog | recent watchdog entries by type, severity and time; message bodies only with include_messages: true; never uid, hostname, location, link or referer |
drupal_menus | menu_link_content | menus and their links, with hierarchy |
drupal_media | file | files and media by type, with size and usage |
drupal_views | views | Views with their displays, paths and status |
drupal_blocks | block | block instances with region, visibility and status |
drupal_moderation | content_moderation | moderation states, pending reviews and recent changes |
drupal_webform | webform | webforms with submission counts; submitted values are never returned |
db_query refuses users, users_field_data, sessions, key_value, key_value_expire, flood,
batch, config, watchdog, node_field_data, taxonomy_term_field_data, file_managed,
webform_submission, webform_submission_data, phpclaw_conversations and phpclaw_messages. The
refusal is QUERY_NOT_PERMITTED and names only the table. Some of that data is readable through
other tools, within their own rules: drupal_entity for users, nodes and terms, read_log for
watchdog, and drupal_config for allowlisted configuration.
Denying tools
Section titled “Denying tools”tool_deny removes tools by name or group:
| Group | Tools |
|---|---|
group:content | drupal_entity, drupal_moderation, drupal_media, drupal_webform |
group:system | drupal_modules, drupal_cron, drupal_cache, drupal_config |
group:nav | drupal_menus, drupal_path_aliases, drupal_blocks, drupal_views |
group:system here holds Drupal tools only; it does not remove db_query or the core tools. Name
those individually.
Approval and PHP files
Section titled “Approval and PHP files”Every agent is built with CliApprovalGate. In a terminal you are asked before a tool changes
something; elsewhere the change is refused. See Security: approval. The MCP
server does not use the approval gate.
file_write may create .php, .phtml and .phar files only under Drush phpclaw:run, not from
the MCP server.
With shell_allowlist empty, shell_exec allows core’s default commands, which include cat,
head, tail and grep. Set your own list to narrow it.
drush phpclaw:run "list active modules"drush pc "show cache bin sizes" --stream
drush phpclaw:mcp-server| Command | Alias | Does |
|---|---|---|
phpclaw:run <message> | pc | send a message; --stream prints tokens as they arrive |
phpclaw:mcp-server | phpclaw-mcp | serve the Drush tool set over stdio; see MCP |
REST API
Section titled “REST API”| Method | Route | Returns |
|---|---|---|
| POST | /api/phpclaw/send | the reply as JSON |
| POST | /api/phpclaw/chat/stream | Server-Sent Events |
Both are ordinary Drupal routes: Drupal authenticates the caller with the providers in
api_auth_providers and checks use phpclaw chat before phpClaw runs. Both also carry Drupal’s
_csrf_request_header_token requirement, so a browser session sends a token from /session/token as
X-CSRF-Token. Drupal applies that check only to requests authenticated by a session cookie, so an
HTTP Basic request needs no token.
To accept another authentication provider, add its id to the list:
drush config:set phpclaw.settings api_auth_providers.2 oauth2To call them from a script with HTTP Basic, enable Drupal’s basic_auth module and use HTTPS:
drush en basic_auth
curl -u 'USERNAME:PASSWORD' \ -X POST https://example.com/api/phpclaw/send \ -H 'Content-Type: application/json' \ -d '{"message": "how many articles are published?"}'The body is JSON with message and an optional conversation_id. send returns ok, text,
provider, model, iterations, tokens, conversation_id and tool_calls. Errors return
{"ok": false, "error": "..."}:
| Status | When |
|---|---|
| 400 | message is missing, empty or longer than 10,000 characters, or the body is not JSON |
| 403 | the conversation belongs to another user |
| 422 | a guard blocked the message |
| 502 | the provider returned an error |
| 500 | anything else failed |
| 503 | the agent could not be built, for example with no provider configured |
chat/stream sends tool_before, tool_after, chunk, done and error events.
Conversations and ownership
Section titled “Conversations and ownership”Each conversation stores its creator’s Drupal user id in phpclaw_conversations.user_id. Conversations
created from Drush store 0. A user sees and opens only their own conversations; opening another
user’s returns 403. manage all phpclaw conversations shows every conversation, including Drush
ones. Analytics counts follow the same rule.
A Drush run stores 0 whenever the Drupal console is active, whatever user the command line names.
The rule is enforced in the memory driver, DrupalDbConversationMemory, which throws
ConversationAccessDeniedException for another user’s conversation. On a site installed before the
owner column existed, phpclaw_update_8004 adds it, so run drush updb; rows without an owner are
visible only with manage all phpclaw conversations.
The agent always uses the database memory driver: conversations in the conversation tables,
everything else in phpclaw_memory.
Using the agent in code
Section titled “Using the agent in code”$agent = \Drupal::service('phpclaw.agent');echo $agent->send('Summarise content published today.')->text;services: mymodule.reporter: class: Drupal\mymodule\Reporter arguments: ['@phpclaw.agent']phpclaw.agent has the Drush tool set; phpclaw.agent.chat has the chat tool set. The factory behind
each returns null when the agent cannot be built, for example with no provider configured.
Extending
Section titled “Extending”Tag your services to register them:
| Tag | Registers |
|---|---|
phpclaw.tool | a tool |
phpclaw.guard | a guard |
phpclaw.skill | a skill |
phpclaw.provider | a provider |
phpclaw.memory_driver | a memory driver |
phpclaw.hook_listener | a listener; set event on the tag, and the service needs a handle() method |
services: mymodule.inventory_tool: class: Drupal\mymodule\Tool\InventoryTool tags: - { name: phpclaw.tool } mymodule.audit_listener: class: Drupal\mymodule\AuditListener tags: - { name: phpclaw.hook_listener, event: agent.after }Events
Section titled “Events”Every lifecycle event is also dispatched on Drupal’s event dispatcher as phpclaw. followed by the
event name. The event object is PhpClaw\Drupal\EventBridge\PhpClawEvent, whose context property
holds the event data. The event names are listed on the Hooks page.