Free

Portando el session recording de Claude Code a un segundo proyecto

El sistema de grabación de how2claude a otro proyecto Rails: 4 archivos, 5 pasos, layering de hooks.


how2claude tiene un set de hooks que auto-graban las sessions de Claude Code — arranca la grabación al empezar a trabajar, añade un checkpoint en cada commit y, al terminar la session, extrae prompts / bash / lista de edits a docs/notes/<feature>/raw.md. El artículo let-claude-record-itself cubrió cómo se montó.

Problema: mi otro proyecto (smarts, un sitio de docs para smart contracts) no tenía nada de esto. Cada vez que quería escribir un artículo después, acababa desenterrando git log y mi memoria, sintiendo que me perdía lo jugoso. Este artículo cuenta cómo porté el sistema de grabación — 4 archivos, 5 minutos en total — pero por el camino apareció una intuición real sobre el layering de hooks: los hooks de Claude Code y los hooks de git operan en capas completamente distintas, y lo que se captura al mezclar herramientas (Amp + Claude Code, por ejemplo) depende de en qué capa lo instales.


4 archivos, una sola imagen

Todo lo de how2claude relacionado con la grabación:

Archivo Capa Rol
bin/recording-state script helper en Python, gestiona el ciclo de vida de .state.json
bin/extract-session-notes script helper en Python, lee el transcript de Claude Code → escribe raw.md
.claude/settings.local.json hook de Claude Code PostToolUse / Stop disparan los scripts de arriba
.git/hooks/post-commit hook de git cada commit llama a recording-state commit para un checkpoint
.gitignore control de ruido mantiene docs/notes/ fuera del repo (las notas son privadas/transitorias)

4 piezas, 4 capas distintas. Esa distinción reaparece abajo.

La clave: el layering de hooks decide qué se captura

Dos sistemas de hooks funcionando a la vez, con ámbitos muy diferentes:

Hooks de Claude Code (definidos en .claude/settings.local.json):
- Ámbito: solo disparan dentro de la herramienta Claude Code
- Disparadores: PostToolUse / Stop / PreToolUse — eventos del ciclo de vida de Claude Code
- Info disponible: nombre de la tool, args, transcript_path (jsonl completo de la session) — cosas que solo Claude Code conoce

Hooks de git (scripts shell bajo .git/hooks/):
- Ámbito: dispara en cualquier evento de git, sin importar quién disparó git
- Disparadores: post-commit / pre-push / etc.
- Info disponible: lo que git sabe (sha, autor, branch, diff)

Consecuencia real: escribir código + commit dentro de Claude Code dispara las dos capas — info de session e info de commit aterrizan en raw.md. Cambiar a Amp (o Cursor, o tecleo a mano) para escribir + commit y solo dispara el hook de git — raw.md recibe el esqueleto del commit pero nada de prompts / bash / edits de la session.

Esto no es un bug — es la restricción de diseño de cada herramienta. Querer detalle de session bajo cualquier herramienta significa instalar tu propia capa de hook por herramienta. El respaldo de git te da el "qué se hizo"; no te da el "qué se pensó, qué se rompió".

Guía de selección:
- Esqueleto agnóstico de herramienta (info de commit, cambios de código) → ponlo en un hook de git
- Carne específica de Claude Code (prompts completos, razonamiento) → ponlo en un hook de Claude Code
- Ambos → instala en ambas capas

Migrar en 5 movimientos

Instalar el mismo setup de grabación en smarts (/home/bob/Work/smarts, un proyecto Rails).

1. Copiar los dos scripts Python

mkdir -p /home/bob/Work/smarts/bin
cp /home/bob/Work/how2claude/bin/recording-state \
   /home/bob/Work/smarts/bin/recording-state
cp /home/bob/Work/how2claude/bin/extract-session-notes \
   /home/bob/Work/smarts/bin/extract-session-notes
chmod +x /home/bob/Work/smarts/bin/{recording-state,extract-session-notes}

Scripts sin modificar — usan la variable de entorno $CLAUDE_PROJECT_DIR para decidir dónde escribir:

def project_dir():
    return os.environ.get("CLAUDE_PROJECT_DIR") or os.getcwd()

def state_path():
    return pathlib.Path(project_dir()) / "docs/notes/.state.json"

Esta es la abstracción clave: Claude Code setea CLAUDE_PROJECT_DIR automáticamente al disparar hooks; nosotros la seteamos a mano dentro del post-commit de git. Ambos lados honran la misma env var, y el script no necesita averiguar "en qué proyecto estoy".

2. Crear .claude/settings.local.json

Una decisión aquí: portar solo los hooks, no la lista de permissions.

El settings.local.json de how2claude tiene más de 100 entradas de permissions.allow — todas específicas de how2claude (curl localhost:3000, bin/rails runner, kamal app exec). Cero sentido arrastrarlas a smarts. smarts acumulará sus propias permissions de forma orgánica al usarlo.

Los hooks son patrones — idénticos entre proyectos. Las permissions son estado del proyecto — distintas entre proyectos.

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write|MultiEdit",
        "hooks": [
          {
            "type": "command",
            "command": "$CLAUDE_PROJECT_DIR/bin/recording-state maybe-start"
          }
        ]
      },
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "$CLAUDE_PROJECT_DIR/bin/recording-state maybe-stop"
          }
        ]
      }
    ],
    "Stop": [
      {
        "hooks": [
          { "type": "command", "command": "$CLAUDE_PROJECT_DIR/bin/extract-session-notes" }
        ]
      }
    ]
  }
}

Tres puntos de disparo:
- Archivo editado → intenta arrancar grabación (maybe-start se auto-chequea: si ya graba, salta; si el tree está limpio, también salta)
- Comando bash ejecutado → intenta parar (maybe-stop es estricto: solo para si "auto-arrancado Y de vuelta en master Y tree limpio" se cumplen a la vez)
- Session termina → extrae el transcript a raw.md

3. Crear .git/hooks/post-commit

3 líneas:

#!/bin/bash
ROOT=$(git rev-parse --show-toplevel 2>/dev/null) || exit 0
CLAUDE_PROJECT_DIR="$ROOT" "$ROOT/bin/recording-state" commit || true

El export manual CLAUDE_PROJECT_DIR=$ROOT es el punto exacto donde el mundo de git y el mundo de Claude Code quedan unidos por la misma env var. El || true garantiza que el hook nunca bloquee un commit.

chmod +x /home/bob/Work/smarts/.git/hooks/post-commit

4. Añadir docs/notes/ al .gitignore

# Session recording notes (transient, for article material)
docs/notes/

Las notas son transitorias + privadas — no quieres raw.md commiteado en PRs; no quieres .state.json contaminando git status. El gitignore de how2claude lo trata igual.

5. Smoke test (con una pequeña sorpresa)

Disparar maybe-start manualmente justo después de instalar:

$ cd /home/bob/Work/smarts && CLAUDE_PROJECT_DIR=$(pwd) ./bin/recording-state maybe-start
[recording] auto-started: contract-to-docs (branch: feat/contract-to-docs)

$ cat docs/notes/.state.json
{
  "feature": "contract-to-docs",
  "started_at": "2026-04-20T17:47:18-04:00",
  "branch": "feat/contract-to-docs",
  "auto_started": true
}

El script pilló correctamente que smarts casualmente estaba en feat/contract-to-docs con tree sucio — auto-arrancó la grabación, infirió el nombre de feature contract-to-docs desde la branch feat/contract-to-docs. Esa lógica en el script:

def branch_to_feature(branch):
    if not branch or branch in ("master", "main"):
        return None
    if "/" in branch:
        return branch.split("/", 1)[1]
    return branch

feat/XX, feature/XX, fix-y pelado → fix-y, master/main → None (que cae a un nombre con timestamp session-YYYYMMDD-HHMM).

La heurística es deliberadamente descerebrada — el nombre de la branch es el tema del trabajo, no hace falta que lo nombres otra vez.

Frontera de visibilidad al mezclar con Amp

Después de instalar me pregunté: de vez en cuando trabajo en Amp — ¿eso se captura? La respuesta es exactamente lo que predice la historia del layering de hooks:

Escenario Hook Claude Code Hook git Qué aterriza en notas
Trabajo en Claude Code + commit ✅ dispara ✅ dispara detalle de session + esqueleto de commit
Trabajo en Amp + commit ❌ no-op ✅ dispara solo esqueleto de commit
Tecleo a mano + commit ❌ no-op ✅ dispara solo esqueleto de commit
Trabajo en Claude Code, sin commit aún ✅ arranca grabación detalle de session (la entrada de commit espera al próximo commit)

Conclusión: suficiente si Claude Code es el driver principal. El esqueleto de commit aterriza siempre; el detalle de session solo aterriza en la ruta de Claude Code. Para artículos "qué se hizo" te apoyas mayormente en los bodies de commit — los prompts / rastros bash de session son bonus, están bien si los tienes pero no son obligatorios.

Si usas Amp mucho, Amp tiene su propio sistema de hooks (no he entrado al detalle); un pequeño script de reenvío disparando recording-state maybe-start/maybe-stop funcionaría igual.

Checklist

Portar el session recording de Claude Code a otro proyecto — los 5 movimientos:

  1. cp dos scripts Python al bin/ del proyecto destino. Sin modificaciones — los scripts honran $CLAUDE_PROJECT_DIR, portables entre proyectos por diseño.
  2. Crear .claude/settings.local.json, solo hooks. No portes la lista de permissions — las permissions son estado de proyecto (distintas por proyecto); los hooks son patrones (idénticos entre proyectos).
  3. Crear .git/hooks/post-commit (3 líneas), export CLAUDE_PROJECT_DIR=$ROOT a mano y llamar a recording-state commit. Ese es el único punto donde el mundo de git y el mundo de Claude Code se conectan por la misma env var.
  4. Añadir docs/notes/ al .gitignore. Las notas son transitorias + privadas, no forman parte del repo.
  5. Smoke test manual: CLAUDE_PROJECT_DIR=$(pwd) ./bin/recording-state maybe-start, verifica que la inferencia branch→feature sea correcta. Si el tree está limpio, el script está diseñado para saltar — no es un bug.

La decisión de diseño real no es "cómo lo porto" — portar es casi cp. Es partir la lógica de grabación en 4 capas distintas, cada una haciendo una cosa clara:

  • Scripts Python: helper sin estado, honra CLAUDE_PROJECT_DIR
  • Hook de Claude Code: eventos internos de la herramienta (carne de session)
  • Hook de git: eventos agnósticos de herramienta (esqueleto de commit)
  • gitignore: control de ruido

Cada una de las 4 puede portarse, reemplazarse o saltarse independientemente (¿quieres soporte para Amp? añade una capa de hook para Amp. ¿Cambiar el formato de notas? edita Python. ¿No quieres que git trackee notas? elimina el post-commit). Claude puede escribir el código bien — pero el juicio sobre "a qué capa pertenece esta funcionalidad" no puede hacerlo por ti. Esa es una llamada sobre tus fronteras de tooling, y es tuya.