Skip to content
Development
Hook

Hooks

What alfred-dev runs automatically, and when. A hook is a command Claude Code fires at a fixed moment, without you asking for it.

From plugin
alfred-dev
11511 skills10 agents20 commands5 hooks
+1
Install
> /plugin marketplace add 686f6c61/alfred-dev
> /plugin install alfred-dev@alfred-dev

Ships with alfred-dev. Installing the plugin gets these hooks.

What fires, and when

SessionStart

Fires once when a session begins, and again after a context compaction. It is where a plugin sets up its environment, or restores state the compaction dropped.

  • Matchesstartup|resume|clear|compact|forkbashbash

SessionEnd

  • python3

UserPromptSubmit

Fires before Claude sees each prompt you send. A plugin can use it to inject context, so the same instruction reaches the model every turn instead of only at session start.

  • python3python3

PreToolUse

  • MatchesWrite|Edit|Bash|mcp__.*python3
  • MatchesBashpython3
  • MatchesReadpython3

PostToolUse

  • MatchesBashpython3python3python3
  • MatchesWrite|Editpython3
Read hooks/hooks.json

In the plugin's words

How alfred-dev describes its own hook set.

Alfred Dev - hooks del ciclo de vida del plugin

Where it lives

  • hooks/activity-capture.pyGitHub
    Read the script
    #!/usr/bin/env python3
    """
    Hook centralizado de captura de actividad para Alfred Dev.
    
    Sustituye a ``memory-capture.py`` y ``commit-capture.py`` proporcionando un
    unico punto de entrada que registra TODA la actividad relevante en la base
    de datos de memoria persistente. Cada evento se almacena con tres niveles
    de detalle:
    
    - **summary**: texto legible en castellano para el dashboard.
    - **payload**: JSON estructurado para filtrado programatico.
    - **content**: texto completo sin truncar para consulta bajo demanda.
    
    Politica de escritura: solo se persiste lo que un humano querría releer.
        En runtime se registran prompts de slash Alfred, commits `git commit` de
        la sesión y escrituras de estado/handoff/UAT/`docs/project/`. No se
        copian Read/Glob/Grep, ni el historial antiguo de Git, ni cada edit de
        código.
    
    Politica fail-open: el hook NUNCA bloquea el flujo de trabajo. Cualquier
    error se imprime en stderr con prefijo ``[activity-capture]`` y se sale
    con codigo 0.
    """
    
    import json
    import os
    import re
    import shlex
    import subprocess
    import sys
    from typing import Optional
    
    
    # ---------------------------------------------------------------------------
    # Constantes
    # ---------------------------------------------------------------------------
    
    # Directorios y patrones excluidos de la captura generica.
    # Son ficheros internos cuya actividad genera ruido sin aportar contexto
    # util al historial del proyecto.
    _EXCLUDED_PREFIXES = (
        ".claude/",
        ".git/",
        "node_modules/",
        "__pycache__/",
        ".venv/",
        "venv/",
        ".mypy_cache/",
        ".pytest_cache/",
    )
    
    # Patron que detecta 'git commit' como comando real, no como argumento
    # de otro comando (grep, echo, etc.). Solo detecta git commit al inicio
    # de la linea o despues de operadores de shell (&&, ||, ;).
    _GIT_COMMIT_RE = re.compile(r"(?:^|&&|\|\||;)\s*git\s+commit\b")
    
    # Comandos triviales que no aportan contexto util al historial.
    # Son comandos de lectura, navegacion o diagnostico que no modifican
    # el estado del proyecto.
    _TRIVIAL_COMMANDS = frozenset({
        "ls", "pwd", "cd", "echo", "cat", "head", "tail", "less", "more",
        "wc", "whoami", "date", "which", "where", "type", "file", "true",
        "false", "clear", "history", "env", "printenv", "set", "export",
        "alias", "unalias", "source", ".", "test", "[",
    })
    
    # Comandos de Alfred que pueden prepararse de forma determinista antes de que
    # el modelo intente resolver el prompt por si mismo.
    _ALFRED_PREFETCH_COMMANDS = frozenset({
        "alfred",
        "audit",
        "map-codebase",
        "discuss",
        "feature",
        "fix",
        "quick",
        "lucius",
        "ship",
        "spike",
        "memory-ui",
    })
    
    # Prefijo para los mensajes de aviso en stderr.
    _LOG_PREFIX = "[activity-capture]"
    
    _HIGH_VOLUME_CONTENT_MAX_LINES = 80
    _HIGH_VOLUME_CONTENT_MAX_CHARS = 4000
    
    
    def _ensure_plugin_root_on_path() -> str:
        """Asegura que la raiz del plugin este disponible para imports.
    
        Los hooks se ejecutan como scripts sueltos desde Claude Code y no siempre
        reciben ``PYTHONPATH`` con la raiz del plugin. Sin este ajuste, imports
        como ``core.memory`` o ``core.memory_config`` pueden fallar incluso cuando
        el plugin esta bien instalado.
    
        Returns:
            Ruta absoluta a la raiz del plugin.
        """
        plugin_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
        if plugin_root not in sys.path:
            sys.path.insert(0, plugin_root)
        return plugin_root
    
    
    # ---------------------------------------------------------------------------
    # Tabla de dispatchers
    # ---------------------------------------------------------------------------
    
    def _build_dispatcher_table():
        """Construye el mapa evento -> funcion de despacho.
    
        Se define como funcion para evitar referencias anticipadas a funciones
        que aun no se han declarado en el momento de la definicion del modulo.
    
        Returns:
            Diccionario con claves de evento y valores de funcion dispatcher.
        """
        return {
            "Write": _dispatch_write,
            "Edit": _dispatch_edit,
            "Bash": _dispatch_bash,
            "Read": _dispatch_read,
            "Glob": _dispatch_glob,
            "Grep": _dispatch_grep,
            "Agent": _dispatch_agent,
            "WebFetch": _dispatch_web_fetch,
            "WebSearch": _dispatch_web_search,
            "NotebookEdit": _dispatch_notebook,
            "UserPromptSubmit": _dispatch_prompt,
            "UserPromptExpansion": _dispatch_prompt,
            "PreCompact": _dispatch_compact,
            "Stop": _dispatch_stop,
        }
    
    
    # ---------------------------------------------------------------------------
    # Funciones auxiliares
    # ---------------------------------------------------------------------------
    
    def _is_memory_enabled() -> bool:
        """Comprueba si la memoria persistente esta habilitada en la configuracion.
    
        Busca el fichero ``alfred-dev.local.md`` en el directorio ``.claude``
        del proyecto actual y verifica que contenga la seccion ``memoria:`` con
        ``enabled: true``.
    
        Returns:
            True si la memoria esta habilitada, False en caso contrario.
        """
        _ensure_plugin_root_on_path()
        try:
            from core.memory_config import is_memory_enabled
        except ImportError:
            return False
    
        return is_memory_enabled(os.getcwd())
    
    
    def _memory_settings():
        """Carga la configuracion efectiva de memoria del proyecto."""
        _ensure_plugin_root_on_path()
        try:
            from core.memory_config import load_memory_config
        except ImportError:
            return {
                "enabled": False,
                "sync_to_native": True,
                "sync_commits_limit": 10,
                "capture_decisions": True,
                "capture_commits": True,
                "retention_days": 365,
            }
        return load_memory_config(os.getcwd())
    
    
    def _is_excluded_path(file_path: str) -> bool:
        """Determina si un fichero debe excluirse de la captura generica.
    
        Se excluyen ficheros internos del plugin, del sistema de control de
        versiones y de caches de herramientas. La comparacion se
  • hooks/dangerous-command-guard.pyGitHub
    Read the script
    #!/usr/bin/env python3
    """
    Hook PreToolUse para Bash: guardia de comandos peligrosos.
    
    Intercepta las ejecuciones de Bash y analiza el comando en busca de patrones
    potencialmente destructivos (borrado catastrofico, force push, destruccion de
    datos, fork bombs, etc.). Si detecta un patron peligroso, bloquea la operacion
    (exit 2) con un aviso explicativo.
    
    Politica de seguridad: fail-closed. Si no se puede parsear la entrada del hook
    o se produce cualquier error inesperado, se bloquea la operacion (exit 2).
    Un hook de seguridad que permite la operacion cuando falla equivale a
    desactivar la proteccion justo cuando mas se necesita. Esta politica es
    coherente con secret-guard.sh, que tambien es fail-closed.
    
    Patrones vigilados:
        - rm -rf / (o ~, o $HOME): borrado catastrofico del sistema o del home.
        - git push --force a main/master: perdida de historial en ramas protegidas.
        - DROP DATABASE / DROP TABLE: destruccion de datos en base de datos.
        - docker system prune -af: eliminacion de volumenes y datos de contenedores.
        - chmod -R 777: permisos inseguros en todo un arbol de directorios.
        - fork bombs: denegacion de servicio local.
        - mkfs / dd sobre dispositivos: destruccion de disco.
        - Escritura directa a dispositivos de bloque.
    """
    
    import json
    import os
    import re
    import shlex
    import sys
    
    
    # --- Patrones peligrosos ---------------------------------------------------
    # Cada tupla contiene (patron_compilado, descripcion_del_riesgo).
    # Los patrones se evaluan en orden; la primera coincidencia bloquea.
    
    _DANGEROUS_PATTERNS = [
        # Borrado catastrofico: rm -rf aplicado a raiz, home o rutas de sistema.
        # Cubre: flags juntas (-rf, -fr), separadas (-r -f), con sudo, y flags largas.
        (
            re.compile(
                r"(?:sudo\s+)?rm\s+"
                r"(?:-[a-zA-Z]*\s+)*"
                r"(?:--\w[\w-]*\s+)*"
                r"(?=-[a-zA-Z]*r)(?=.*-[a-zA-Z]*f)"
                r".*\s+(/\s|/\*|/$|~\s|~$|~\/|\$HOME|\$\{HOME\}|/usr|/etc|/var|/boot|/System)"
            ),
            "Borrado catastrofico: rm -rf sobre directorio raiz o de sistema",
        ),
        # Force push a ramas protegidas
        (
            re.compile(
                r"git\s+push\s+.*"
                r"(--force\b|-f\b)"
                r".*\b(main|master)\b"
            ),
            "Force push a rama protegida (main/master): riesgo de perdida de historial",
        ),
        # Variante: force push sin rama explicita (se asume rama actual)
        (
            re.compile(
                r"git\s+push\s+--force-with-lease\s*$"
                r"|git\s+push\s+-f\s*$"
                r"|git\s+push\s+--force\s*$"
            ),
            "Force push sin rama explicita: verifica que no estas en main/master",
        ),
        # Destruccion de base de datos
        (
            re.compile(r"DROP\s+(DATABASE|TABLE|SCHEMA)\s", re.IGNORECASE),
            "Destruccion de datos: DROP DATABASE/TABLE/SCHEMA",
        ),
        # Docker prune agresivo (cubre -af, -fa, -a -f, -f -a y combinaciones con otros flags)
        (
            re.compile(
                r"docker\s+system\s+prune\s+.*(-af|-fa)\b"
                r"|docker\s+system\s+prune\s+.*-a\b.*-f\b"
                r"|docker\s+system\s+prune\s+.*-f\b.*-a\b"
            ),
            "Docker system prune con -af: elimina todos los datos de contenedores",
        ),
        # Permisos inseguros
        (
            re.compile(r"chmod\s+(-R\s+)?777\s+/"),
            "Permisos inseguros: chmod 777 recursivo sobre directorio raiz",
        ),
        # Fork bomb (variantes comunes en bash)
        (
            re.compile(r":\(\)\s*\{\s*:\s*\|\s*:\s*&\s*\}\s*;?\s*:"),
            "Fork bomb: denegacion de servicio local",
        ),
        # Formateo de disco
        (
            re.compile(r"mkfs\.\w+\s+/dev/"),
            "Formateo de disco: mkfs sobre dispositivo de bloque",
        ),
        # dd sobre dispositivo de bloque
        (
            re.compile(r"dd\s+.*of=/dev/(sd|hd|nvme|vd|xvd)"),
            "Escritura directa a dispositivo de bloque con dd",
        ),
        # Escritura a dispositivo via redireccion
        (
            re.compile(r">\s*/dev/(sd|hd|nvme|vd|xvd)"),
            "Redireccion de salida a dispositivo de bloque",
        ),
        # git reset --hard a remote (destructivo en combinacion con push)
        (
            re.compile(r"git\s+reset\s+--hard\s+origin/(main|master)"),
            "git reset --hard a origin/main: descarta todos los cambios locales",
        ),
    ]
    
    _SAFE_ALFRED_HELPER_SUBCOMMANDS = frozenset({
        "allow-stop-once",
        "blocked",
        "consume-prefetch",
        "discuss",
        "in-progress",
        "map-codebase",
        "memory-ui",
        "next",
        "pause",
        "progress",
        "quick",
        "resume",
        "search",
        "standup",
        "validate",
        "verify",
    })
    
    _SHELL_CONTROL_TOKENS = frozenset({
        ";",
        "&&",
        "||",
        "|",
        ">",
        ">>",
        "<",
        "<<",
        "2>",
        "&>",
        "&>>",
    })
    
    _SHELL_CONTROL_SUBSTRINGS = (
        "$(",
        "`",
    )
    
    _SAFE_CAPTURE_SUFFIXES = (
        "2>&1",
        "2>/dev/null",
        "2>/dev/null 2>&1",
    )
    
    
    _DROP_SQL_REGEX = re.compile(r"\bDROP\s+(DATABASE|TABLE|SCHEMA)\b", re.IGNORECASE)
    _DEVICE_REDIRECT_REGEX = re.compile(r">\s*/dev/(sd|hd|nvme|vd|xvd)\w*", re.IGNORECASE)
    _SHELL_WRAPPERS = frozenset({"sh", "bash", "zsh"})
    _SQL_CLIENTS = frozenset({"psql", "mysql", "mariadb", "sqlite3", "duckdb"})
    
    
    def _command_basename(token: str) -> str:
        return os.path.basename(token or "")
    
    
    def _strip_leading_wrappers(tokens):
        """Elimina wrappers neutros al principio: sudo, env y variables inline."""
        idx = 0
        while idx < len(tokens):
            token = tokens[idx]
            base = _command_basename(token)
    
            if base == "sudo":
                idx += 1
                while idx < len(tokens) and tokens[idx].startswith("-"):
                    idx += 1
                continue
    
            if base == "env":
                idx += 1
                while idx < len(tokens) and "=" in tokens[idx] and not tokens[idx].startswith("-"):
                    idx += 1
                continue
    
            if "=" in token and not token.startswith("-"):
                name, _value = token.split("=", 1)
                if name.replace("_", "").isalnum():
                
  • hooks/evidence-guard.pyGitHub
    Read the script
    #!/usr/bin/env python3
    """
    Hook PostToolUse para Bash: verificacion de evidencia.
    
    Implementa el patron «evidencia antes que afirmaciones» inspirado en el
    plugin Ralph Loop. Registra en un fichero temporal las ejecuciones de
    tests detectadas durante la sesion, de modo que otros componentes del
    plugin (stop-hook, check_gate) puedan verificar que cuando un agente
    afirma que los tests pasan, efectivamente se ejecutaron.
    
    La logica de deteccion y almacenamiento esta en ``evidence_guard_lib.py``
    para que otros modulos puedan importarla sin ejecutar el hook.
    
    Politica fail-open: nunca bloquea el flujo de trabajo.
    """
    
    import json
    import os
    import sys
    
    # Anadir el directorio de hooks al path para importar la libreria
    _HOOKS_DIR = os.path.dirname(os.path.abspath(__file__))
    if _HOOKS_DIR not in sys.path:
        sys.path.insert(0, _HOOKS_DIR)
    
    from evidence_guard_lib import (
        is_test_command,
        detect_test_result,
        record_evidence,
    )
    
    _LOG_PREFIX = "[evidence-guard]"
    
    
    def _get_tool_result(data: dict) -> dict:
        """Normaliza payloads actuales e historicos del runtime de hooks."""
        for key in ("tool_response", "tool_result", "tool_output"):
            value = data.get(key)
            if isinstance(value, dict):
                return value
            if isinstance(value, str) and value:
                return {"output": value}
        return {}
    
    
    def main():
        """Punto de entrada del hook PostToolUse para Bash.
    
        Lee el JSON de stdin, detecta si el comando ejecutado es un runner
        de tests, analiza su resultado y lo registra como evidencia.
    
        Politica fail-open: cualquier error inesperado se reporta en stderr
        pero el hook siempre termina con exit 0 para no bloquear el flujo.
        """
        try:
            data = json.load(sys.stdin)
        except (ValueError, json.JSONDecodeError) as e:
            print(
                f"{_LOG_PREFIX} Aviso: no se pudo leer la entrada: {e}",
                file=sys.stderr,
            )
            sys.exit(0)
    
        try:
            tool_input = data.get("tool_input", {})
            tool_result = _get_tool_result(data)
    
            command = tool_input.get("command", "")
            if not command or not is_test_command(command):
                sys.exit(0)
    
            # Extraer la salida del comando
            stdout = tool_result.get("stdout", "") or tool_result.get("output", "")
            stderr = tool_result.get("stderr", "")
            exit_code = tool_result.get("exit_code")
            output = f"{stdout}\n{stderr}"
    
            # Detectar resultado y registrar evidencia
            result = detect_test_result(output, exit_code=exit_code)
            record_evidence(command, result)
    
            if result == "unknown":
                print(
                    f"{_LOG_PREFIX} Tests ejecutados pero no se pudo determinar "
                    f"el resultado. El comando se ha registrado igualmente.",
                    file=sys.stderr,
                )
        except Exception as e:
            print(
                f"{_LOG_PREFIX} Error inesperado (fail-open, no bloquea): {e}",
                file=sys.stderr,
            )
    
        sys.exit(0)
    
    
    if __name__ == "__main__":
        main()
    
  • hooks/evidence_guard_lib.pyGitHub
    Read the script
    #!/usr/bin/env python3
    """
    Libreria de verificacion de evidencia para Alfred Dev.
    
    Contiene las funciones de deteccion y almacenamiento de evidencia de
    tests separadas del punto de entrada del hook. Esto permite que otros
    modulos (tests, stop-hook, orchestrator) importen las funciones sin
    ejecutar el hook.
    
    El hook ``evidence-guard.py`` importa desde aqui y solo añade el
    punto de entrada ``main()`` con lectura de stdin.
    """
    
    import json
    import os
    import re
    import sys
    from datetime import datetime, timezone
    from typing import Any, Dict, List, Optional
    
    
    # --- Constantes ---
    
    # Fichero donde se registra la evidencia de tests.
    _EVIDENCE_FILENAME = "alfred-evidence.json"
    
    # Tiempo maximo (en segundos) para considerar una evidencia como reciente.
    EVIDENCE_MAX_AGE_SECONDS = 600  # 10 minutos
    
    # Patrones de runners de tests.
    _CMD_POS = r"(?:^|[;&|])\s*"
    
    TEST_RUNNERS = [
        rf"{_CMD_POS}pytest\b",
        r"\bpython3?\s+-m\s+pytest\b",
        rf"{_CMD_POS}vitest\b",
        rf"{_CMD_POS}jest\b",
        rf"{_CMD_POS}mocha\b",
        r"\bcargo\s+test\b",
        r"\bgo\s+test\b",
        r"\bnpm\s+test\b",
        r"\bnpm\s+run\s+test\b",
        r"\bpnpm\s+test\b",
        r"\bpnpm\s+run\s+test\b",
        r"\bbun\s+test\b",
        r"\bbun\s+run\s+test\b",
        r"\byarn\s+test\b",
        r"\byarn\s+run\s+test\b",
        r"\bpython\s+-m\s+unittest\b",
        rf"{_CMD_POS}phpunit\b",
        rf"{_CMD_POS}rspec\b",
        r"\bmix\s+test\b",
        r"\bdotnet\s+test\b",
        r"\bmaven\s+test\b",
        r"\bmvn\s+test\b",
        r"\bgradle\s+test\b",
    ]
    
    # Patrones de fallo en salida de tests.
    # Se buscan en contexto de salida de runners, así que usamos patrones
    # específicos para evitar falsos positivos con palabras como «fail-safe»
    # o «OK» en texto genérico.
    FAILURE_PATTERNS = [
        r"(?<!\d )\bFAIL[ED]*\b(?![-_])",  # FAIL, FAILED pero no fail-safe, fail_over ni "0 failed"
        r"[1-9]\d*\s+failures?\b",       # "1 failure", "3 failures" (excluye "0 failures")
        r"\bfailing\b",
        r"[Tt]ests?\s+failed",
        r"ERRORS?\s*[:=]",              # "ERRORS:" o "ERROR=" pero no "ERROR" suelto
        r"Assertion(?:Error|Failed)",    # AssertionError → typo corregido a ambas formas
        r"test\s+result:\s+FAILED",
        r"Build\s+FAILED",
        r"[1-9]\d*\s+failed",            # "1 failed", "3 failed" (excluye "0 failed")
        r"not\s+ok\s+\d+",              # TAP format: "not ok 1 - test name"
    ]
    
    # Patrones de exito en salida de tests.
    SUCCESS_PATTERNS = [
        r"\d+\s+passed",
        r"[Tt]ests?\s+passed",
        r"test\s+result:\s+ok\b",
        r"All\s+tests\s+passed",
        r"\d+\s+passing\b",
        r"\bPASS(?:ED)?\b",             # PASS o PASSED
        r"\d+\s+tests?\s+complete",
        r"[Tt]ests?\s+run:\s+\d+",      # "Tests run: N" — formato JUnit/Maven/TestNG
        r"(?m)^ok\s+\S+",               # go test: "ok  github.com/foo/bar 0.003s" (multiline para anclar ^)
    ]
    
    
    # --- Funciones de deteccion ---
    
    def is_test_command(command: str) -> bool:
        """Determina si un comando corresponde a la ejecucion de tests.
    
        Args:
            command: comando Bash ejecutado (cadena completa).
    
        Returns:
            True si el comando coincide con un runner de tests.
        """
        return any(re.search(pattern, command) for pattern in TEST_RUNNERS)
    
    
    def detect_test_result(output: str, exit_code: Optional[Any] = None) -> str:
        """Analiza la salida de un comando de tests para determinar el resultado.
    
        Los patrones de fallo tienen prioridad sobre los de exito para
        cubrir el caso de tests parciales (3 passed, 1 failed). Si la salida
        no permite decidir pero el runner devolvio un codigo de salida distinto
        de cero, se considera fallo igualmente.
    
        Args:
            output: salida completa del comando (stdout + stderr).
            exit_code: codigo de salida del proceso, si esta disponible.
    
        Returns:
            ``"pass"``, ``"fail"`` o ``"unknown"``.
        """
        has_fail = any(
            re.search(pattern, output, re.IGNORECASE)
            for pattern in FAILURE_PATTERNS
        )
        has_pass = any(
            re.search(pattern, output, re.IGNORECASE)
            for pattern in SUCCESS_PATTERNS
        )
    
        if has_fail:
            return "fail"
        if exit_code is not None:
            try:
                if int(exit_code) != 0:
                    return "fail"
            except (TypeError, ValueError):
                pass
        if has_pass:
            return "pass"
        return "unknown"
    
    
    # --- Gestion del fichero de evidencia ---
    
    def _evidence_path(project_dir: Optional[str] = None) -> str:
        """Calcula la ruta del fichero de evidencia."""
        base = project_dir or os.getcwd()
        return os.path.join(base, ".claude", _EVIDENCE_FILENAME)
    
    
    def _load_evidence(project_dir: Optional[str] = None) -> List[Dict[str, Any]]:
        """Carga el registro de evidencia existente."""
        path = _evidence_path(project_dir)
        try:
            with open(path, "r", encoding="utf-8") as f:
                data = json.load(f)
            if isinstance(data, list):
                return data
            print(
                f"[evidence-guard] Aviso: el fichero de evidencia no contiene "
                f"una lista. Se descarta el contenido.",
                file=sys.stderr,
            )
            return []
        except FileNotFoundError:
            return []
        except json.JSONDecodeError as e:
            print(
                f"[evidence-guard] Aviso: fichero de evidencia corrupto "
                f"({path}): {e}. Se perdera la evidencia anterior.",
                file=sys.stderr,
            )
            return []
        except OSError as e:
            print(
                f"[evidence-guard] Aviso: no se pudo leer la evidencia "
                f"({path}): {e}.",
                file=sys.stderr,
            )
            return []
    
    
    def _save_evidence(
        records: List[Dict[str, Any]],
        project_dir: Optional[str] = None,
    ) -> None:
        """Guarda el registro de evidencia. Trunca a 50 registros."""
        path = _evidence_path(project_dir)
        dir_path = os.path.dirname(path)
        os.makedirs(dir_path, exist_ok=True)
    
        trimmed = records[-50:]
        try:
            with open(path, "w", encoding="utf-8") as f:
                json.dump(trimmed, f, en
  • hooks/prompt-route.pyGitHub
    Read the script
    #!/usr/bin/env python3
    """UserPromptSubmit fail-open: inyecta la ruta Alfred si el texto no trae slash."""
    
    from __future__ import annotations
    
    import json
    import os
    import sys
    
    
    def _plugin_root() -> str:
        return os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
    
    
    def main() -> int:
        try:
            data = json.load(sys.stdin)
        except Exception:
            return 0
        if not isinstance(data, dict):
            return 0
    
        prompt = (
            data.get("prompt")
            or data.get("user_prompt")
            or data.get("content")
            or ""
        )
        if not isinstance(prompt, str) or not prompt.strip():
            return 0
    
        try:
            root = _plugin_root()
            if root not in sys.path:
                sys.path.insert(0, root)
            from core.prompt_route import classify_prompt, render_route_hint
    
            match = classify_prompt(prompt)
            if not match:
                return 0
            hint = render_route_hint(match)
            print(
                json.dumps(
                    {
                        "hookSpecificOutput": {
                            "hookEventName": "UserPromptSubmit",
                            "additionalContext": hint,
                        }
                    },
                    ensure_ascii=False,
                )
            )
        except Exception as exc:
            print(f"[prompt-route] aviso: {exc}", file=sys.stderr)
        return 0
    
    
    if __name__ == "__main__":
        raise SystemExit(main())
    
  • hooks/quality-gate.pyGitHub
    Read the script
    #!/usr/bin/env python3
    """
    Hook PostToolUse para Bash: quality gate de tests.
    
    Intercepta la salida de comandos Bash para detectar si se han ejecutado
    tests y, en caso afirmativo, analizar si han fallado. Cuando detecta
    fallos, informa por stderr con la voz de "El Rompe-cosas" (QA).
    
    Delega la deteccion de comandos y resultados a ``evidence_guard_lib``
    para mantener una unica fuente de verdad en los patrones.
    
    Solo actua sobre comandos que coincidan con runners de tests conocidos.
    El resto de comandos Bash pasan sin inspeccion.
    """
    
    import json
    import os
    import sys
    
    # Anadir el directorio de hooks al path para importar la libreria
    _HOOKS_DIR = os.path.dirname(os.path.abspath(__file__))
    if _HOOKS_DIR not in sys.path:
        sys.path.insert(0, _HOOKS_DIR)
    
    from evidence_guard_lib import is_test_command, detect_test_result
    
    
    def _get_tool_result(data: dict) -> dict:
        """Normaliza payloads actuales e historicos del runtime de hooks."""
        for key in ("tool_response", "tool_result", "tool_output"):
            value = data.get(key)
            if isinstance(value, dict):
                return value
            if isinstance(value, str) and value:
                return {"output": value}
        return {}
    
    
    def main():
        """Punto de entrada del hook.
    
        Lee el JSON de stdin, extrae el comando ejecutado y su salida,
        y determina si hay tests fallidos. Si los hay, emite un aviso
        por stderr con la voz de El Rompe-cosas.
        """
        try:
            data = json.load(sys.stdin)
        except (ValueError, json.JSONDecodeError) as e:
            print(
                f"[quality-gate] Aviso: no se pudo leer la entrada del hook: {e}. "
                f"La monitorizacion de tests esta desactivada para este comando.",
                file=sys.stderr,
            )
            sys.exit(0)
    
        tool_input = data.get("tool_input", {})
        tool_result = _get_tool_result(data)
    
        command = tool_input.get("command", "")
    
        if not command or not is_test_command(command):
            sys.exit(0)
    
        stdout = tool_result.get("stdout", "") or tool_result.get("output", "")
        stderr_out = tool_result.get("stderr", "")
        exit_code = tool_result.get("exit_code")
        output = f"{stdout}\n{stderr_out}"
    
        if detect_test_result(output, exit_code=exit_code) == "fail":
            print(
                "\n"
                "[El Rompe-cosas] He pillado tests rotos\n"
                "\n"
                "Los tests no pasan. Sorpresa: ninguna.\n"
                "No se avanza con tests en rojo. Asi funciona esto.\n"
                "\n"
                "Repasa la salida, corrige los fallos y vuelve a ejecutar.\n"
                "Ese edge case que no contemplaste? Lo encontre.\n",
                file=sys.stderr,
            )
    
        sys.exit(0)
    
    
    if __name__ == "__main__":
        main()
    
  • hooks/secret-guard.pyGitHub
  • hooks/secret-guard.shGitHub
  • hooks/sensitive-read-guard.pyGitHub
  • hooks/session-bootstrap.shGitHub
  • hooks/session-end.pyGitHub
  • hooks/session-start.shGitHub

All 12 scripts are listed above. The source is inlined for 6 of them, starting with whatever hooks.json actually runs. See all of them in the repo.

Read the script before you install anything that runs on your machine. This is the one part of a plugin that acts without being asked.

Ships withalfred-dev

Tu equipo de desarrolladores en un plugin. 10 agentes, 11 skills planas, 18 comandos /alfred-dev:*. Memoria persistente, quality gates con evidencia y MCP local.

Get the whole plugin, auto-invoked