Skip to content

English · Español

Lab 02 — Añadir un hook de pre-commit personalizado

Pre-requisito: lee ../theory/02-engineering-hygiene.md. Objetivo: escribir un hook de pre-commit local (no desde un repo público) que fuerce una regla específica del proyecto. No mires solutions/02-precommit-ref.md hasta haberlo hecho funcionar.

§1 Contexto

.pre-commit-config.yaml declara los hooks. La mayoría de hooks viven en repos públicos (pre-commit-hooks, ruff-pre-commit, etc.) y están versionados por tag. Pero para reglas específicas del proyecto — cosas que solo importan a este currículo — escribes un hook local: un script en este repo que pre-commit invoca.

§2 Tu tarea

Escribe un hook local de pre-commit llamado forbid-pickle-in-checkpoint-load que:

  1. Escanee archivos .py staged buscando las cadenas pickle.load( o pickle.loads(.
  2. Permita matches en:
  3. Archivos cuya ruta empiece con tests/.
  4. Líneas que incluyan el comentario al final # nosec safe-source: <reason>.
  5. Rechace cualquier otro match con un mensaje de error claro que mencione safetensors como alternativa.

Esto es una defensa real de security/THREATS.md: pickle.load sobre un checkpoint que vino de fuente no confiable ejecuta código arbitrario al cargar. Fase 16+ usa exclusivamente safetensors.

§3 Restricciones

  • Python puro, sin deps extra. Usa solo la librería estándar.
  • El script vive en scripts/precommit/forbid_pickle_loads.py.
  • Debe ser ejecutable directamente (uv run python scripts/precommit/forbid_pickle_loads.py file1.py file2.py) y salir con código distinto de cero si encuentra algo.
  • Debe estar conectado en .pre-commit-config.yaml como hook repo: local con language: python.
  • La salida debe ser path:line:col: <message> en líneas ofensivas — el mismo formato que usa ruff, así los LSP de editor pueden navegar.

§4 Tests

Añade tests/test_forbid_pickle_loads.py cubriendo:

  1. Un archivo que contenga pickle.load(...) → hook sale con 1, imprime una detección.
  2. Un archivo en tests/ que contenga pickle.load(...) → hook sale con 0.
  3. Un archivo con pickle.load(...) # nosec safe-source: round-trip test → hook sale con 0.
  4. Un archivo con pickle.loads(b"...") → hook sale con 1.
  5. Un archivo con solo import pickle → hook sale con 0.

No leas input solo desde fixtures de filesystem — capsys de pytest + pasar rutas de archivos temporales vía tmp_path es más limpio.

§5 Condiciones de parada

  • uv run python scripts/precommit/forbid_pickle_loads.py <file> produce los códigos de salida esperados.
  • El hook se dispara en un git commit -am 'test' real donde añadiste pickle.load(...) en sitio no whitelisteado.
  • pytest tests/test_forbid_pickle_loads.py pasa.
  • just lint está en verde.
  • Commit: lab: phase-00 add forbid-pickle-loads pre-commit hook.

§6 Qué habrás aprendido

  • Cómo pre-commit invoca hooks locales (lista de archivos como argv, código de salida = pasa/falla).
  • La diferencia entre una puerta de estilo (ruff) y una puerta de política (este — sobre seguridad).
  • Por qué # nosec con una razón es mejor que supresión general (auditable, acotado, removible).
  • El argumento de safetensors como ejemplo concreto.

§7 Pistas (úsalas con moderación)

  1. pre-commit pasa los archivos staged como args posicionales al hook. sys.argv[1:] es la lista.
  2. Usa tokenize o un regex simple sobre líneas — parsing AST completo es excesivo para una política de búsqueda de cadena.
  3. La config repo: local en pre-commit:
    - repo: local
      hooks:
        - id: forbid-pickle-loads
          name: Forbid pickle.load(s) outside tests/
          entry: uv run python scripts/precommit/forbid_pickle_loads.py
          language: system
          types: [python]
    
  4. El hook también debería funcionar cuando se ejecuta con cero archivos (no-op, exit 0).

Si recurres a solutions/02-precommit-ref.md antes de completar esto, marca dod.lab_attempted_before_solutions: false. La honestidad importa aquí.