Runs Drupal coding standards (phpcs) on custom module files after each edit and tells Claude what to fix.

Claude Code mods para proyectos Drupal que usan DDEV.
Pide confirmación antes de comandos que destruyen datos o código que git no puede recuperar: reinstalar el sitio, vaciar tablas, borrar entidades en bloque, rm -rf, ddev delete, restaurar un snapshot, o descartar cambios de git. Una regla deny existente se respeta igual.
Tras editar o crear un fichero de un módulo custom (.php, .module, .install, .inc, .theme, .profile), ejecuta phpcs (estándares Drupal y DrupalPractice) dentro de DDEV sobre ese fichero y añade el informe al contexto para que Claude lo corrija en el mismo turno.
Por defecto busca los módulos en web/modules/custom/. Si el proyecto usa otra raíz (p. ej. docroot/), fija la variable de entorno DRUPAL_WEBROOT:
claude mcp add ... # no aplica aquí; para plugins usa:
claude plugin install drupal-phpcs --marketplace o0Charly0o/drupal-mods -s user -e DRUPAL_WEBROOT=docroot
Si DDEV no está levantado o phpcs no existe, el hook nunca bloquea la edición.
Barra de estado: rama de git y cambios pendientes, si el contenedor web de DDEV está arriba, y el progreso (hecho/total) de cada change activo en openspec/changes/.
claude plugin install drupal-guard --marketplace o0Charly0o/drupal-mods -s user
claude plugin install drupal-phpcs --marketplace o0Charly0o/drupal-mods -s user
claude plugin install ddev-status --marketplace o0Charly0o/drupal-mods -s user
Con -s user quedan disponibles en todos los proyectos; fuera de un proyecto Drupal/DDEV simplemente no se activan.
MIT
hooks/register.ts 55 lines1import type { EngineInterface, Register, ToolCallResult } from 'claude-code'
2
3const DEFAULT_WEBROOT = 'web'
4const CHECKED = /\.(php|module|install|inc|theme|profile)$/
5
6/**
7 * After Claude edits or writes a PHP file of a custom module, runs phpcs
8 * (Drupal, DrupalPractice) inside DDEV on that file. Problems are added to
9 * what Claude reads after the edit, so it fixes them in the same turn.
10 *
11 * The webroot defaults to `web/`; set DRUPAL_WEBROOT (e.g. `docroot`) if the
12 * project uses a different one.
13 */
14export const register: Register = on => {
15 on('tool.call', { tool: 'Edit' }, async ($, e, next) => check($, e.file_path, await next(e)))
16 .catch(($, e, next) => next(e))
17 on('tool.call', { tool: 'Write' }, async ($, e, next) => check($, e.file_path, await next(e)))
18 .catch(($, e, next) => next(e))
19}
20
21/**
22 * Runs phpcs on an edited file and adds its report to the result.
23 */
24async function check<R extends ToolCallResult>($: EngineInterface, path: string, ran: R): Promise<R> {
25 const webroot = (await $.env.get('DRUPAL_WEBROOT')) || DEFAULT_WEBROOT
26 const custom = `${webroot}/modules/custom/`
27 const at = path.indexOf(custom)
28 if (ran.deny !== undefined || ran.isError === true || at < 0 || !CHECKED.test(path)) {
29 return ran
30 }
31
32 const root = path.slice(0, at)
33 const relative = path.slice(at)
34 try {
35 const { exitCode, stdout } = await $.process.run(
36 ['ddev', 'exec', 'vendor/bin/phpcs', '--standard=Drupal,DrupalPractice', '--report=emacs', relative],
37 { cwd: root, timeoutMs: 60000 },
38 )
39 if (exitCode === 0) {
40 $.ui.status('phpcs ✓')
41 return ran
42 }
43 const report = stdout.trim().split('\n').slice(0, 30).join('\n')
44 $.ui.status(`phpcs ✗ ${relative.split('/').pop() ?? relative}`)
45 return {
46 ...ran,
47 context: [...(ran.context ?? []), `phpcs (Drupal, DrupalPractice) found problems in ${relative}; fix them:\n${report}`],
48 }
49 }
50 catch {
51 // DDEV stopped or phpcs missing: never block the edit.
52 return ran
53 }
54}
55