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

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 31 lines1import 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