How to Use AI Coding Agents on Drupal Projects with AGENTS.md, Tests and MCP
Point a general-purpose AI coding agent at a Drupal 11 site and it may reach for variable_get() or hook_menu(), Drupal 7 APIs that modern Drupal removed. It may also invent a field called field_body when your site uses field_content.
This tutorial sets up the following on a Drupal 11 or Drupal CMS project in DDEV, using a workflow that applies across different coding agents:
- a short root
AGENTS.mdfile that tells the agent where things live and which checks to run; - reusable Drupal skills from the contributed AI Best Practices project;
- a local loop of four checks, PHPCS, PHPStan, Rector and PHPUnit, that the agent runs on its own work;
- optionally, live site inspection through Drush or the Model Context Protocol (MCP), with your provider keys kept out of Git.
If you do only one thing, add the AGENTS.md file and PHPStan. In the test for this tutorial, PHPStan stopped mistakes that the coding-standards check let through.
None of this replaces your own review. Drupal founder Dries Buytaert put the rule in the title of a 16 March 2026 blog post: "Never submit code you don't understand".
Why AI Agents Get Drupal Wrong
Drupal.org's community guide AI Coding Tools for Drupal Development warns that AI coding agents "do not know Drupal", "cannot validate their own output", "hallucinate confidently" and "can generate insecure code". In Drupal 11 work, that can appear in four ways:
- Mixed major-version APIs. Models suggest removed functions such as
variable_get(),db_select()orhook_menu(), or write@Plugindocblock annotations where modern Drupal uses PHP attributes (#[Block],#[Action],#[Hook]). - Service locator anti-patterns. Models call static
\Drupal::service()or\Drupal::entityTypeManager()methods inside classes instead of injecting services through the constructor. - Missing cacheability and access checks. Generated render arrays may omit
#cachecontexts, tags and max-age, or skipaccessCheck(TRUE)on entity queries. - Hidden site architecture. Content types, field machine names, Views and workflows live in configuration and the database, not just PHP files, so an agent reading the code alone may have to guess them.
What You Need
- A Drupal 11.4 project (11.4.11 is the latest stable patch release as of 29 October 2026, following the 11.4.10 security release) or a Drupal CMS 2.2 project, running locally in DDEV. Drupal CMS 2.2.7's DDEV settings require DDEV 1.25.0 or later.
- A coding agent that reads
AGENTS.mdor Agent Skills files, such as Claude Code, Codex, Cursor, Gemini CLI, GitHub Copilot, Google Antigravity or OpenCode. Step 2 shows where each one looks.
The steps were tested on Drupal CMS 2.2.7 with Drupal core 11.4.11 and PHP 8.3 in DDEV in October 2026. Local testing took place on 25–26 October 2026 using DDEV 1.24.10, with the template's DDEV 1.25.0 version check relaxed. The tests covered Steps 1 to 4, including configuration synchronisation, a Kernel test and the three first-session prompts. Each resulting module passed the four checks when rerun independently in the author's test environment.
Step 1: Brief the Agent with a Short AGENTS.md
AGENTS.md is a plain Markdown file that supported coding agents use as project instructions. It tells the agent where the code lives, what not to touch and how to check the work.
Drupal CMS already ships an AGENTS.md file in the project root. It tells agents that the site is managed with Composer and asks them to check first whether to run Composer and Drush through DDEV (preferred) or locally. It lists common workflows such as adding a module and importing configuration, and it sets guardrails: no committed secrets or vendor/ folder, no edits to core or contributed projects in place, and custom code in web/modules/custom and web/themes/custom. On Drupal CMS, add your own rules below that text.
The core/AGENTS.md in Drupal 12's code (issue #3585894) is intended for core contributors; a site project does not need it.
Before writing rules, give the agent real configuration to read. On a fresh DDEV site there is no config/sync/ folder yet. DDEV generates its own settings.ddev.php, which settings.php includes, and when settings.php names no sync folder it uses web/sites/default/files/sync, inside the files folder that Drupal CMS keeps out of Git.
Set the folder in settings.php itself, outside the web root as Drupal core's settings file advises, rather than in settings.ddev.php, which DDEV manages and may overwrite. Add this line to web/sites/default/settings.php:
$settings['config_sync_directory'] = '../config/sync';
Then export the site's configuration once:
ddev drush config:export -y
Drupal CMS also keeps settings.php out of Git, so add the line on each copy of the site; the config/sync/ folder can be committed.
Now add rules like these below the Drupal CMS text. The four checks at the end are installed in Step 3:
# Project instructions for coding agents
## Environment
- Drupal 11.4 on PHP 8.3, running in DDEV. Run PHP, Composer, Drush and the checks inside DDEV (`ddev composer ...`, `ddev drush ...`, `ddev exec ...`).
- Custom modules live in `web/modules/custom/`. Exported configuration lives in `config/sync/`.
## Before writing code
- Check real field and bundle names in `config/sync/field.field.*.yml` and `config/sync/node.type.*.yml`. Never invent them.
- Scaffold modules, plugins and forms with `ddev drush generate` instead of writing boilerplate from memory.
## Drupal 11 coding rules
- Use PHP attributes, not docblock annotations: `#[Block]`, `#[Action]`, `#[Hook]` (hooks go in classes under `src/Hook/`), and PHPUnit attributes such as `#[Group]` in tests.
- Inject services through the constructor. Controllers and forms that extend `ControllerBase` or `FormBase`, and plugins that extend `PluginBase` and implement `ContainerFactoryPluginInterface`, autowire constructor arguments, so a hand-written `create()` is usually unnecessary. Never call `\Drupal::service()` or `\Drupal::entityTypeManager()` inside classes.
- In plugins, declare promoted constructor properties `protected`, not `private` or `readonly`.
- Call `->accessCheck(TRUE)` on entity queries, or `FALSE` with a documented reason.
- Add cache contexts, tags and max-age to render arrays whose output depends on the user, content or configuration.
- After changing configuration, export it with `ddev drush config:export` so it lands in `config/sync/`.
## Tests
- Add a Kernel test for new behaviour in `tests/src/Kernel/`.
- Mark Kernel and Functional test classes with `#[RunTestsInSeparateProcesses]`.
## Checks to run before finishing
Run all four and fix every reported error before presenting changes:
1. `ddev exec vendor/bin/phpcs --standard=Drupal,DrupalPractice web/modules/custom`
2. `ddev exec vendor/bin/phpstan analyse`
3. `ddev exec vendor/bin/rector process web/modules/custom --dry-run`
4. `ddev exec vendor/bin/phpunit -c web/core web/modules/custom`
The rules match Drupal 11.4. Core autowires these constructors for controllers since Drupal 10.2, plugins since Drupal 11.3.0 (issue #3452852) and forms since Drupal 11.4.0. Kernel and Functional test classes without #[RunTestsInSeparateProcesses] trigger a deprecation since Drupal 11.3.0, and Drupal 12.0.0 will throw an exception.
Keep the file short and let the checks do the policing: phpcs and phpstan report the file and line of each problem they find.
Step 2: Add Drupal Skills with AI Best Practices
Former Drupal core committer Angie Byron (webchick) created the AI Best Practices for Drupal project. It is not a module you enable on the site. It ships Agent Skills files (SKILL.md): instruction files, in an open format, that an agent loads for a specific kind of task. It has no tagged release yet, so you install its development branch.
For a new site, the quick start on the project page runs a one-line installer that creates a DDEV Drupal site with the skills installed; it can also manage an existing DDEV project. Read the script before running it:
curl -fsSL https://aibp.drupalstarforge.ai/install.sh -o install-aibp.sh
less install-aibp.sh
bash install-aibp.sh
For an existing Composer-managed project in DDEV, install the package as a Composer plugin:
ddev composer config allow-plugins.drupal/ai_best_practices true
ddev composer require --dev drupal/ai_best_practices:@dev
ddev composer drupal-ai skills-list
Installed with Composer, the package:
- copies the skills into
.agents/skills/; - updates a managed
<!-- ai-best-practices:start -->…<!-- ai-best-practices:end -->block at the top of your rootAGENTS.md, without overwriting your own rules below it; - creates
CLAUDE.mdandGEMINI.mdfiles, each containing@AGENTS.md, for the two tools whose instruction files have their own names.
On a Drupal CMS site, it also copies five skills that the Canvas Translate module ships. To leave them out, list them under exclude_skills in the package's ai_best_practices.yaml settings file, for example canvas_translate:openspec-propose.
Where Each Coding Agent Looks
Each tool's documentation, linked in the table, gives these files and folders:
| Coding agent | Project instructions | Project skills folder |
|---|---|---|
| Claude Code | CLAUDE.md, which the package points to AGENTS.md; recent versions also read AGENTS.md when there is no CLAUDE.md | .claude/skills/ |
| Codex | AGENTS.md | .agents/skills/ |
| Cursor | AGENTS.md | .agents/skills/ or .cursor/skills/ |
| Gemini CLI | GEMINI.md, which the package points to AGENTS.md | .agents/skills/ or .gemini/skills/ |
| GitHub Copilot | AGENTS.md | .agents/skills/, .github/skills/ or .claude/skills/ |
| Google Antigravity | AGENTS.md or GEMINI.md | .agents/skills/ |
| OpenCode | AGENTS.md | .agents/skills/, .opencode/skills/ or .claude/skills/ |
The package's default folder, .agents/skills/, serves every agent in the table except Claude Code, which reads .claude/skills/. The package picks .claude/skills/ itself when it detects Claude Code, but inside DDEV it may not. If you use Claude Code, check skills.output_directory in ai_best_practices.yaml and run ddev composer drupal-ai skills-sync after changing it. The package keeps that file out of Git, so each developer can choose.
Which Skills a Newcomer Uses First
The project has 13 skills, in three groups:
- Everyday module and theme work.
drupal-render-pipelinecovers render arrays and cacheability metadata.drupal-configurationcovers choosing between config, state and settings, config schema and the export workflow.drupal-automated-testingcovers Unit, Kernel, Functional and FunctionalJavascript tests.drupal-accessibilityroutes todrupal-a11y-fapi(Form API and render arrays),drupal-a11y-dom(Twig and CSS) anddrupal-a11y-dynamic(JavaScript and AJAX).drupal-ckeditor5-pluginscovers custom editor plugins. - Contributing on Drupal.org.
drupal-gitlabcovers theglabCLI and the issue-fork workflow ongit.drupalcode.org,drupal-writing-documentationcovers README files, change records and module help, anddrupal-a11y-qacovers accessibility issue reports. - Maintaining skills.
drupal-skill-authoringcovers writing and validating skills, anddrupal-expert-correctionsturns expert corrections into evaluation cases.
A skill talks to the agent directly. The drupal-render-pipeline skill warns it that the render pipeline "is where AI agents confabulate most frequently, and the errors are especially dangerous because they produce working code in development that breaks under caching in production". The drupal-automated-testing skill tells it: "Do not reach for Unit tests first."
The first group applies to almost any custom module. The others stay out of the way until a task needs them: an agent reads each skill's name and description at the start and loads the full instructions only when it uses the skill.
Step 3: Make the Agent Check Its Own Work in DDEV
Install Drupal's development tools and palantirnet/drupal-rector, then copy its starter rector.php file to your project root:
ddev composer config allow-plugins.phpstan/extension-installer true
ddev composer config allow-plugins.dealerdirect/phpcodesniffer-composer-installer true
ddev composer require --dev drupal/core-dev:^11.4 palantirnet/drupal-rector -W
cp vendor/palantirnet/drupal-rector/rector.php .
Drupal CMS needs the first two lines because its project template does not allow these Composer plugins; without them the require stops with "phpstan/extension-installer contains a Composer plugin which is blocked by your allow-plugins config". drupal/recommended-project already allows them.
drupal/core-dev brings PHPCS with Drupal's Coder rules, PHPStan with mglaman/phpstan-drupal and its deprecation rules, and PHPUnit 11. With phpstan/extension-installer allowed, PHPStan loads the Drupal extensions automatically.
Create a phpstan.neon file in the project root:
parameters:
level: 5
paths:
- web/modules/custom
Kernel tests need a database. Point PHPUnit at DDEV's database, then restart the project:
ddev config --web-environment-add="SIMPLETEST_DB=mysql://db:db@db/db,SIMPLETEST_BASE_URL=http://web"
ddev restart
Without SIMPLETEST_DB, PHPUnit stops every Kernel test with "There is no database connection so no tests can be run".
The four commands at the end of the sample AGENTS.md are the loop. A new Drupal CMS site has no web/modules/custom folder, so the checks encounter missing-path errors until the first custom module exists. Then run them yourself and confirm they pass, so you can trust the agent's report. Each catches a different kind of mistake:
phpcscatches formatting and discouraged functions.phpstan, withphpstan-drupal, flags nonexistent methods, deprecated APIs and Drupal-specific traps.drupal-rectorreports known deprecations under the--dry-runflag, and rewrites them when run without it.phpunitruns the Unit and Kernel tests inweb/modules/custom.
In the test, PHPStan caught mistakes that PHPCS missed. PHPCS does not check entity queries for accessCheck(), and it warns about \Drupal calls only in blocks, controllers, forms, formatters, widgets and services. A plain class that did both passed PHPCS; PHPStan flagged both. On Drupal 10 and later, a query without accessCheck() also throws an exception when it runs.
PHPStan also rejected a private readonly constructor property in a modern-style block plugin. Drupal plugins inherit DependencySerializationTrait, which does not support private properties and, on PHP versions before 8.4, cannot restore readonly ones. Declared protected, the plugin and a Kernel test for it passed all four checks.
A First Session with the Agent
The agents in the Step 2 table load their instruction file when a session starts, so you do not need to ask them to read AGENTS.md separately. Commit the project to Git first, so that git diff shows only what the agent changes. Three prompts check that the setup works; the expected answers come from a fresh Drupal CMS 2.2.7 site with the Starter recipe.
1. Check that the rules are loaded.
Which checks must you run before you finish a change?
Expect the four commands from your AGENTS.md. A vague answer, or a list of other tools, means the agent may not have loaded the file: check its row in the Step 2 table.
2. Check that it reads the site's real configuration.
Using only the files in config/sync/, list this site's content types and their fields, with machine names.
With the Starter recipe, expect one content type, machine name page, labelled "Utility page", with eight fields: field_content, field_description, field_featured_image, field_tags and four SEO fields, field_seo_title, field_seo_description, field_seo_image and field_seo_analysis. It may also mention the five media types and the Tags vocabulary in the same folder. On your own site, compare the answer with the content types page, /admin/structure/types.
A field that is not there, such as field_body, is the guessing this setup is meant to stop.
3. Ask for a small feature.
Create a custom module with a block that lists the five newest published Utility pages, showing each title linked to its page and the page's Description field. Add a Kernel test, then run the four checks and fix anything they report.
The result will vary between agents and runs. If the agent followed the sample AGENTS.md, you should find:
- a module in
web/modules/custom/and a block class with the#[Block]attribute; - the description read from
field_description, the field found in prompt 2; - services injected through the constructor, with the block implementing
ContainerFactoryPluginInterfaceand no\Drupal::calls; accessCheck(TRUE)on the query, and cache tags and contexts on the block's output;- a test in
tests/src/Kernel/marked#[RunTestsInSeparateProcesses]; - a report that all four checks pass. If the agent stops at an error, ask it to fix it.
Some agents also enable the module and export the configuration. On Drupal CMS, that export adds a Canvas component for the new block to config/sync/, so look for it in the diff when you review the change yourself (Step 5).
Step 4: Let the Agent Read Live Site State, Not Your API Keys
When a task depends on live site configuration or content, give the agent a way to inspect the site instead of letting it guess.
- Drush and the Drupal 11.4 native CLI. Inside DDEV,
ddev drush config:get,ddev drush config:statusandddev drush updatedb:statuslet an agent read active configuration and pending updates directly. Drupal 11.4 also adds an experimental native CLI;ddev exec vendor/bin/dr listshows its commands. Let an agent run commands that change the site, such asddev drush updatedborddev drush cr, only on a local development copy. - Model Context Protocol (MCP) tools. For typed, permission-checked Drupal operations over HTTP, MCP Server and Tool API connect through MCP Server Tool Bridge, with OAuth token scoping from MCP Server OAuth and Simple OAuth. The experimental Agent Access recipe installs them together, and each coding agent registers MCP servers in its own settings file. Apart from Simple OAuth, these projects have only alpha or beta releases, which Drupal security advisories do not cover, so keep write-capable tools on local development sites. The DropTimes' Drupal AI modules tracker lists each one's current release and coverage.
- Provider keys in Key, never in
AGENTS.mdor exported YAML. When you test a Drupal AI provider module locally, store its API key with the Key module in an environment variable or a file outside the webroot and database dumps, and add Encrypt where you need encrypted storage. The key then stays out ofconfig/sync/, which the agent reads and you commit.
Step 5: Review Every Line Before You Commit or Contribute
Passing static analysis does not mean a patch solves the right problem, avoids access bypasses, or belongs in custom code rather than an existing contributed module.
Before committing or uploading code to a Drupal.org merge request:
- Read the full diff (
git diff) line by line and remove unrequested refactors or comments. - Confirm that you can explain why every change is needed and how it behaves on edge cases. Developers who accept agent output without reading the underlying APIs may struggle to debug or maintain it when a review question comes back.
- When contributing on Drupal.org, disclose significant AI assistance in the issue summary, comment or merge request description (or use the template's AI disclosure section) as required by Drupal.org's AI contribution policy.
References
-
Never submit code you don't understand (16 March 2026)
-
AI Best Practices for Drupal | Drupal.org (26 March 2026)


