SLOPSHOPPER

drupal-guard

Asks for confirmation before destructive commands (site install, database drops, entity deletes, rm -rf...).

newguard
v0.1.0MITupdated 2026-10-20o0Charly0o/drupal-mods/drupal-guard
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 31 lines
1import type { Register } from 'claude-code'
2
3/**
4 * Commands that destroy data or code that git cannot bring back. Each one is
5 * put to the person in the permission dialog, whatever the allow rules say.
6 */
7const DESTRUCTIVE: ReadonlyArray<[RegExp, string]> = [
8  [/\b(site:install|site-install)\b|\bdrush\s+si\b/, 'reinstalls the Drupal site (wipes the database)'],
9  [/\bsql:drop\b|\bsql-drop\b|\bDROP\s+(TABLE|DATABASE)\b|\bTRUNCATE\b/i, 'drops or empties database tables'],
10  [/\bentity:delete\b|\bentity-delete\b/, 'deletes Drupal entities in bulk'],
11  [/\bddev\s+(delete|poweroff)\b|\bddev\s+snapshot\s+restore\b|\bddev\s+import-db\b/, 'replaces or removes the DDEV database or project'],
12  [/\brm\s+(-[a-zA-Z]*r[a-zA-Z]*f|-[a-zA-Z]*f[a-zA-Z]*r)\b/, 'removes files recursively (rm -rf)'],
13  [/\bfind\b[^|]*\s-delete\b/, 'deletes files found by find'],
14  [/\bgit\s+(clean\s+-[a-zA-Z]*f|stash\s+drop|branch\s+-D)\b/, 'discards git work'],
15  [/\bconfig:import\b|\bcim\b/, 'overwrites active configuration with config/default'],
16]
17
18export const register: Register = on => {
19  on('tool.check', { tool: 'Bash' }, async ($, e, next) => {
20    const command = String((e.input as { command?: unknown }).command ?? '')
21    const hit = DESTRUCTIVE.find(([pattern]) => pattern.test(command))
22    const verdict = await next(e)
23
24    // Only tighten: a deny from the rules stays a deny.
25    if (hit === undefined || verdict.decision === 'deny') {
26      return verdict
27    }
28    return { decision: 'ask', reason: `drupal-guard: this command ${hit[1]}.` }
29  }).catch(($, e, next) => next(e))
30}
31