How to Use AI Coding Agents on Drupal Projects with AGENTS.md, Tests and MCP

Project Rules, Automated Checks and Live Site Context Instead of Guesswork
The Drop Times tutorial graphic titled "When AI-Generated Drupal Code Looks Right but Isn't" shows a code editor with a magnifying glass highlighting potential issues. The text explains that AI coding agents can produce convincing code while missing Drupal-specific requirements and that project instructions and automated checks help uncover problems before production.

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.md file 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() or hook_menu(), or write @Plugin docblock 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 #cache contexts, tags and max-age, or skip accessCheck(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.md or 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 root AGENTS.md, without overwriting your own rules below it;
  • creates CLAUDE.md and GEMINI.md files, 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 agentProject instructionsProject skills folder
Claude CodeCLAUDE.md, which the package points to AGENTS.md; recent versions also read AGENTS.md when there is no CLAUDE.md.claude/skills/
CodexAGENTS.md.agents/skills/
CursorAGENTS.md.agents/skills/ or .cursor/skills/
Gemini CLIGEMINI.md, which the package points to AGENTS.md.agents/skills/ or .gemini/skills/
GitHub CopilotAGENTS.md.agents/skills/, .github/skills/ or .claude/skills/
Google AntigravityAGENTS.md or GEMINI.md.agents/skills/
OpenCodeAGENTS.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-pipeline covers render arrays and cacheability metadata. drupal-configuration covers choosing between config, state and settings, config schema and the export workflow. drupal-automated-testing covers Unit, Kernel, Functional and FunctionalJavascript tests. drupal-accessibility routes to drupal-a11y-fapi (Form API and render arrays), drupal-a11y-dom (Twig and CSS) and drupal-a11y-dynamic (JavaScript and AJAX). drupal-ckeditor5-plugins covers custom editor plugins.
  • Contributing on Drupal.org. drupal-gitlab covers the glab CLI and the issue-fork workflow on git.drupalcode.org, drupal-writing-documentation covers README files, change records and module help, and drupal-a11y-qa covers accessibility issue reports.
  • Maintaining skills. drupal-skill-authoring covers writing and validating skills, and drupal-expert-corrections turns 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:

  • phpcs catches formatting and discouraged functions.
  • phpstan, with phpstan-drupal, flags nonexistent methods, deprecated APIs and Drupal-specific traps.
  • drupal-rector reports known deprecations under the --dry-run flag, and rewrites them when run without it.
  • phpunit runs the Unit and Kernel tests in web/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 ContainerFactoryPluginInterface and 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:status and ddev drush updatedb:status let an agent read active configuration and pending updates directly. Drupal 11.4 also adds an experimental native CLI; ddev exec vendor/bin/dr list shows its commands. Let an agent run commands that change the site, such as ddev drush updatedb or ddev 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.md or 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 of config/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:

  1. Read the full diff (git diff) line by line and remove unrequested refactors or comments.
  2. 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.
  3. 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.

Disclosure: This content is produced with the assistance of AI.

Note: The vision of this web portal is to help promote news and stories around the Drupal community and promote and celebrate the people and organizations in the community. We strive to create and distribute our content based on these content policy. If you see any omission/variation on this please reach out to us at #thedroptimes channel on Drupal Slack and we will try to address the issue as best we can.

Related Drupal Initiatives

Related People

Upcoming Events

Latest Opportunities