SLOPSHOPPER

drupal-phpcs

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

newguardstatusprocess
v0.1.0MITupdated 2026-10-20o0Charly0o/drupal-mods/drupal-phpcs
A shopper browsing a rack in a slop shop
README

drupal-mods

Claude Code mods para proyectos Drupal que usan DDEV.

Plugins

drupal-guard

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.

drupal-phpcs

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.

ddev-status

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/.

Instalar

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.

License

MIT

Source 1 files
hooks/register.ts 55 lines
1import 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