Skip to content
Development
Hook

Hooks

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

From plugin
lore
79 skills1 agent1 command4 hooks

Claude Code

> /plugin marketplace add andresanemic/lore-plugin
> /plugin install lore@lore-plugin

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.

  • node "${CLAUDE_PLUGIN_ROOT}/hooks/codex-guard.mjs" session_start

PreToolUse

  • Matchesapply_patch|Edit|Write|MultiEdit|NotebookEditnode "${CLAUDE_PLUGIN_ROOT}/hooks/codex-guard.mjs" pre_tool_use
  • MatchesAgent|Tasknode "${CLAUDE_PLUGIN_ROOT}/hooks/delivery-observer.mjs" pre

PostToolUse

  • MatchesAgent|Tasknode "${CLAUDE_PLUGIN_ROOT}/hooks/delivery-observer.mjs" post
  • node "${CLAUDE_PLUGIN_ROOT}/hooks/codex-guard.mjs" post_tool_use

PreCompact

  • Matchesmanual|autonode "${CLAUDE_PLUGIN_ROOT}/hooks/codex-guard.mjs" pre_compact
Read hooks/hooks.json

Where it lives

  • hooks/baraja-entry.mjsGitHub
    Read the script
    // hooks/baraja-entry.mjs — Punto de entrada para la baraja de perturbaciones.
    // Ofrece una carta interna al empezar una tarea y prepara el prompt una sola vez.
    // No es una skill: es un hook que cualquier skill puede invocar.
    
    import { tirarCarta } from "../scripts/baraja.mjs";
    import { loadOperationState, saveOperationState, withOperationLock } from "../skills/vespi/core/operation-state.mjs";
    import { offerTaskCard } from "../skills/vespi/core/cards.mjs";
    
    const ESTANCAMIENTO_PALABRAS = [
      "loop", "stuck", "atascado", "repetiendo", "no avanzo", "no sé qué hacer",
      "mismo error", "tres veces", "sin progreso", "estancado", "bucle",
    ];
    
    export function detectarEstancamiento(texto) {
      // Legacy text classifier; never authorizes an offer or prompt.
      const lower = texto.toLowerCase();
      return ESTANCAMIENTO_PALABRAS.some((palabra) => lower.includes(palabra));
    }
    
    export function ofrecerCarta({ semilla = null, fuente = null } = {}) {
      return tirarCarta({ semilla, fuente });
    }
    
    export async function inyectarCartaEnPrompt({ operacion = null, root = null, taskId = null, semilla = null } = {}) {
      if (!root || !operacion?.id) return null;
      // Leer, ofrecer y guardar es una sola secuencia: sin el cerrojo, dos tareas a la vez se pisan la carta.
      return withOperationLock(root, () => inyectarCarta({ operacion, root, taskId, semilla }));
    }
    
    async function inyectarCarta({ operacion, root, taskId, semilla }) {
      const stored = await loadOperationState(root, operacion.id);
      if (!stored || ["closed", "cancelled"].includes(stored.state)) return null;
      let current = stored;
      // A task only receives its own offer. Re-reading its prompt is idempotent and
      // returns that same card; it must neither consume another task's offer nor
      // create a duplicate.
      let offer = taskId
        ? current.perturbations?.find(item => item.task_id === taskId)
        : current.perturbations?.findLast(item => item.response_status === "offered" && !item.prompt_prepared_at);
      if (!offer && taskId) {
        current = offerTaskCard(current, { taskId, semilla });
        offer = current.perturbations.at(-1);
      }
      if (!offer) return null;
      if (!offer.prompt_prepared_at) {
        await saveOperationState(root, { ...current, perturbations: current.perturbations.map(item => item.id === offer.id ? { ...item, prompt_prepared_at: new Date().toISOString() } : item) });
      }
      const carta = offer.card;
    
      return {
        role: "system",
        cardId: offer.id,
        content: `[Baraja] Perturbación ofrecida (no requerida): "${carta.texto}" — Fuente: ${carta.fuente}. Total disponibles: ${carta.total}.`,
      };
    }
    
  • hooks/codex-guard.mjsRunsGitHub
    Read the script
    #!/usr/bin/env node
    // SessionStart + PreToolUse + PostToolUse + UserPromptSubmit hook — Lore Plugin.
    //
    // Codex adapter of the same guard. `SessionStart` records a silent per-session
    // baseline and never evaluates Lore state; since 2.4.8 it adds one static check:
    // when the cwd is a federated bot whose always-on block does not declare its
    // load, it emits exactly one line, else zero bytes. `PostToolUse` evaluates
    // only once the current Lore digest departs from that baseline — i.e. once THIS
    // session has touched the Lore. A receipt that was already stale when the
    // session opened stays silent until the first in-session Lore edit.
    // Fails open on any error.
    //
    // `UserPromptSubmit` es otra cosa y no es la guardia: es el recordatorio por turno
    // (R28). Pone delante, en cada turno y sin que la persona lo vea, el registro que esta
    // en vigor y donde vive el estado. No evalua nada del Lore y no bloquea nunca.
    
    import { existsSync, readFileSync, readSync } from "node:fs";
    import { join } from "node:path";
    
    import {
      anotarDesconocidos,
      evaluateState,
      foreignWrites,
      formatIntervention,
      unknownWrites,
    } from "./lore-guard.mjs";
    import { inyeccion, nivel as nivelActivo, estadoDir } from "./lore-turno.mjs";
    import {
      loreDeparted,
      nextTurn,
      readReceipt,
      readSessionBaseline,
      readSessionRoot,
      snapshot,
      takeCompactMark,
      writeCompactMark,
      writeReceipt,
      writeSessionBaseline,
      writeSessionRoot,
    } from "./lore-state.mjs";
    
    const event = process.argv[2];
    const OK = () => process.exit(0);
    let data;
    
    try {
      const maxInputBytes = 1_048_576;
      const chunks = [];
      let totalBytes = 0;
      while (totalBytes <= maxInputBytes) {
        const chunk = Buffer.alloc(Math.min(65_536, maxInputBytes - totalBytes + 1));
        const bytesRead = readSync(0, chunk, 0, chunk.length, null);
        if (bytesRead === 0) break;
        totalBytes += bytesRead;
        if (totalBytes > maxInputBytes) OK();
        chunks.push(chunk.subarray(0, bytesRead));
      }
      const raw = Buffer.concat(chunks).toString("utf8");
      data = JSON.parse(raw || "{}");
    } catch {
      OK();
    }
    
    if (!["session_start", "pre_tool_use", "post_tool_use", "user_prompt_submit", "pre_compact"].includes(event)) OK();
    if (event === "post_tool_use" && typeof data.turn_id !== "string") OK();
    
    const root = typeof data.cwd === "string" && data.cwd ? data.cwd : process.cwd();
    const sessionId = typeof data.session_id === "string" ? data.session_id : null;
    
    // El recordatorio por turno (R28) corre antes que todo lo demás, y con el guard alrededor:
    // la guardia solo tiene algo que decir cuando ESTA sesion toco el Lore, y esto se dice
    // siempre. El payload de Codex trae `nivel`; Claude Code no lo trae nunca, asi que la
    // perilla se lee del archivo y del entorno, y el campo es solo una via para probarla.
    function recordatorioDe(turno) {
      try {
        const bruto = typeof data.nivel === "string" ? data.nivel : nivelActivo({ estadoDir: estadoDir() });
        return inyeccion({ raiz: root, turno, nivel: bruto });
      } catch {
        return { inyectar: false, texto: null };
      }
    }
    
    function emite(turno, aviso = null) {
      const r = recordatorioDe(turno);
      if (!r.inyectar || !r.texto) {
        if (aviso) process.stdout.write(JSON.stringify({ systemMessage: aviso }));
        return;
      }
      process.stdout.write(JSON.stringify({
        ...(aviso ? { systemMessage: aviso } : {}),
        hookSpecificOutput: {
          hookEventName: turno === null ? "SessionStart" : "UserPromptSubmit",
          additionalContext: r.texto,
        },
      }));
      OK();
    }
    
    // Un turno por peticion, y el numero sale de un contador por sesion. Un `turn_id`
    // ausente o repetido no es una razon para callar: el recordatorio es un piso, y un piso
    // que se salta porque falto un campo no es un piso.
    // El recordatorio por turno (R28), y la razon por que NO esta en `hooks.json`.
    //
    // El evento existe y el guard lo contesta, porque es el canal que el host con forma
    // Codex usa para hablar por turno. Lo que NO se hace es engancharlo a Claude Code, y la
    // razon no es una preferencia: se midio. En Claude Code 2.1.284, sesion fresca con el
    // plugin cargado y dos turnos, el `UserPromptSubmit` con `additionalContext` SI llega al
    // modelo — y el modelo lo clasifica como instruccion de la persona y lo narra:
    // "Contexto de tu hook: «Turno 2: hablo cercana…». Lo tomo como indicaciones tuyas."
    // Eso es exactamente el defecto que 2.4.7 registro y por el que se retiro el adaptador,
    // y en esta version sigue igual. Un recordatorio "que la persona no ve" del que el
    // agente responde no es un recordatorio privado: es una frase mas en la conversacion.
    //
    // Lo que si es privado en Claude Code es la marca de la linea de estado, que no entra al
    // contexto del modelo por construccion. Por eso R46 la puso ahi y no en la prosa.
    if (event === "user_prompt_submit") {
      let turno = 1;
      try {
        turno = nextTurn(sessionId, root);
      } catch {
        /* sin tmp: el numero no avanza y el recordatorio sigue llegando */
      }
      emite(turno);
      // Este evento no evalúa nada del Lore y no bloquea nunca: termina aquí. Sin esta salida,
      // cuando no había recordatorio que inyectar la ejecución seguía hasta el código de
      // PostToolUse y armaba la línea base de la sesión desde un turno humano.
      OK();
    }
    
    
    // --- `pre_compact`: la marca silenciosa (2.4.9) ---------------------------------
    //
    // Cero bytes de salida, y esa es toda la restricción. No puede ser de otra manera: el host
    // DESCARTA el `systemMessage` de `PreCompact` y el stdout plano de un hook de command no se
    // entrega al modelo en este evento; y aunque lo hiciera, un texto que el modelo lee como
    // instrucción de la persona es exactamente el defecto medido en `UserPromptSubmit`. Lo que queda
    // es un archivo en el tmp de la sesión, que no es contexto: es un hecho en disco.
    //
    // Se ancla en la MISMA raíz que el resto de la guardia (`readSessionRoot`), no en el `cwd`, que
    // en Claude Code yaderivó cuando se compacta. Falla abierto: una marca que no se puede escribir
    // deja pasar la comp
  • hooks/delivery-observer.mjsRunsGitHub
    Read the script
    #!/usr/bin/env node
    // PreToolUse + PostToolUse de la herramienta de agentes (Agent/Task) — Lore Plugin.
    //
    // Observa, desde el host, que un encargo de Vespi cruzo el limite hacia un trabajador, y lo entrega cuando
    // el coordinador lo pide por referencia. Dos formas de prompt le conciernen:
    //
    // - La referencia corta `@lore-task <operacion> <tarea> sha256:<huella>`. En PreToolUse el hook lee la
    //   operacion en `<cwd>/FASES.md`, renderiza el encargo canonico y, solo si su huella es la referida,
    //   reescribe la entrada de la herramienta con ese encargo (`updatedInput`). Asi el metodo cruza byte a byte
    //   sin que el modelo lo copie: la prueba real del 2026-10-20 mostro que una copia de 10 KB pierde bytes.
    // - El encargo ya renderizado (cabecera `# Task <t> of operation <id>` y bloque de metodo), que solo se
    //   registra.
    //
    // Escribe una linea por evento en `<cwd>/.lore-deliveries.jsonl` con huellas, nunca con el contenido. Nunca
    // decide el permiso de la herramienta (no devuelve `permissionDecision`), nunca bloquea y falla abierto.
    //
    // Limites, dichos aqui para que nadie los infiera de un verde: ve la intencion de lanzar (Pre) y la
    // respuesta de la herramienta (Post), no la conducta del trabajador; un lanzamiento por Bash no pasa por
    // aqui; y el registro lo puede escribir el mismo usuario del sistema operativo. `confirm-delivery` contrasta
    // cada par con el transcript de la sesion y declara esa integridad como `same-os-user`.
    
    import { closeSync, constants, lstatSync, openSync, readFileSync, writeSync } from "node:fs";
    import { execFileSync } from "node:child_process";
    import { createHash } from "node:crypto";
    import { join, resolve } from "node:path";
    
    const AGENT_TOOLS = new Set(["Agent", "Task"]);
    const TASK_HEADER = /^# Task (\S+) of operation (\S+)$/m;
    const TASK_REFERENCE = /^@lore-task (\S+) (\S+) sha256:([0-9a-f]{64})$/;
    const sha256 = (text) => createHash("sha256").update(text).digest("hex");
    function appendLocal(path, text) {
      try { if (lstatSync(path).isSymbolicLink()) throw new Error("delivery log is a symbolic link"); }
      catch (error) { if (error?.code !== "ENOENT") throw error; }
      const fd = openSync(path, constants.O_WRONLY | constants.O_APPEND | constants.O_CREAT | (constants.O_NOFOLLOW ?? 0), 0o600);
      try { writeSync(fd, text); } finally { closeSync(fd); }
    }
    
    function excludeDeliveryLog(cwd) {
      try {
        const path = resolve(cwd, execFileSync("git", ["-C", cwd, "rev-parse", "--git-path", "info/exclude"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim());
        const entry = ".lore-deliveries.jsonl";
        let current = "";
        try { current = readFileSync(path, "utf8"); } catch (error) { if (error?.code !== "ENOENT") throw error; }
        if (!current.split(/\r?\n/).includes(entry)) appendLocal(path, `${current && !current.endsWith("\n") ? "\n" : ""}${entry}\n`);
      } catch { /* no git repository or exclude unavailable: observation stays best-effort */ }
    }
    
    // La huella del metodo que trae la definicion del trabajador del kit que el host carga en esta sesion
    // (`${CLAUDE_PLUGIN_ROOT}/agents/lore-worker.md`). null si no se puede leer.
    function kitWorkerMethodSha() {
      const root = process.env.CLAUDE_PLUGIN_ROOT;
      if (!root) return null;
      let text;
      try { text = readFileSync(join(root, "agents", "lore-worker.md"), "utf8"); } catch { return null; }
      const start = text.indexOf("\n<<<METHOD\n");
      const end = text.lastIndexOf("\nMETHOD>>>\n");
      if (start < 0 || end <= start) return null;
      return sha256(text.slice(start + "\n<<<METHOD\n".length, end));
    }
    
    async function main() {
      const event = process.argv[2];
      if (event !== "pre" && event !== "post") return;
      const data = JSON.parse(readFileSync(0, "utf8") || "{}");
      if (!AGENT_TOOLS.has(data.tool_name)) return;
      const prompt = data.tool_input?.prompt;
      if (typeof prompt !== "string") return;
      const cwd = typeof data.cwd === "string" && data.cwd ? data.cwd : process.cwd();
      const base = {
        event,
        tool_use_id: typeof data.tool_use_id === "string" ? data.tool_use_id : null,
        session_id: typeof data.session_id === "string" ? data.session_id : null,
        transcript_path: typeof data.transcript_path === "string" ? data.transcript_path : null,
        tool_name: data.tool_name,
      };
      const post = event === "post"
        ? { agent_id: data.tool_response?.agentId ?? data.tool_response?.agent_id ?? null, response_sha256: sha256(JSON.stringify(data.tool_response ?? null)) }
        : {};
      const write = (line) => {
        excludeDeliveryLog(cwd);
        appendLocal(join(cwd, ".lore-deliveries.jsonl"), `${JSON.stringify({ ...line, at: new Date().toISOString() })}\n`);
      };
    
      const reference = prompt.trim().match(TASK_REFERENCE);
      if (reference) {
        const [, operation, task, referred] = reference;
        const line = { ...base, operation, task, prompt_sha256: referred, reference: true, ...post };
        if (event === "post") return write(line);
        let commission;
        try {
          const { readOperation } = await import("../skills/vespi/core/vespi.mjs");
          const { taskPrompt } = await import("../skills/vespi/core/coordinator.mjs");
          const artifact = await readOperation({ root: cwd, id: operation });
          commission = taskPrompt(artifact, task);
        } catch (error) {
          return write({ ...line, expanded: false, refused: `commission not found: ${error?.message ?? String(error)}` });
        }
        if (commission.prompt_sha256 === referred) {
          write({ ...line, route: "prompt", expanded: true });
          process.stdout.write(JSON.stringify({ hookSpecificOutput: { hookEventName: "PreToolUse", updatedInput: { ...data.tool_input, prompt: commission.prompt } } }));
          return;
        }
        if (commission.commission_sha256 === referred) {
          // El encargo sin metodo solo va al trabajador de ESTE plugin, por su identidad completa
          // (`<nombre del manifiesto>:lore-worker`), y solo si su definicion trae el metodo fijado. El nombre
          // corto lo puede usar cualquier otro plugin (H1, revision d
  • hooks/inyecta-lus.mjsGitHub
    Read the script
    // hooks/inyecta-lus.mjs — Inyecta la esencia de LUS en el contexto de los agentes.
    // Cuando una skill de Lore Plugin se activa, el agente lee la esencia y sabe que es un jardinero.
    
    import { readFileSync } from "node:fs";
    import { resolve, join } from "node:path";
    import { fileURLToPath } from "node:url";
    
    const __dirname = fileURLToPath(import.meta.url);
    const kitRoot = resolve(__dirname, "..");
    const esenciaPath = join(kitRoot, "..", "..", "..", "investigacion-cientifica", "proyectos", "LUS", "esencia", "esencia.md");
    
    function leeEsencia() {
      try { return readFileSync(esenciaPath, "utf8"); } catch { return null; }
    }
    
    export function inyectaLus({ skill = null } = {}) {
      const esencia = leeEsencia();
      if (!esencia) return null;
    
      return {
        role: "system",
        content: `[LUS — Esencia del programa]\n\n${esencia}\n\n${skill ? `[Skill activada: ${skill}]` : ""}`,
      };
    }
    
    export function inyectaEnPrompt({ messages = [], skill = null }) {
      const inyeccion = inyectaLus({ skill });
      if (!inyeccion) return messages;
      // Insertar la inyección como primer mensaje de sistema
      const resultado = [...messages];
      const primerSysteme = resultado.findIndex((m) => m.role === "system");
      if (primerSysteme >= 0) {
        resultado.splice(primerSysteme, 0, inyeccion);
      } else {
        resultado.unshift(inyeccion);
      }
      return resultado;
    }
    
  • hooks/lore-guard.mjsGitHub
    Read the script
    import { appendFileSync, chmodSync, existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, statSync } from "node:fs";
    import { homedir, tmpdir } from "node:os";
    import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
    
    import { SESSION_DIR } from "./lore-state.mjs";
    
    export const MATERIAL_GROWTH_BYTES = 8_192;
    
    function inside(root, target) {
      const rel = relative(root, target);
      return rel === "" || (!rel.startsWith("..") && !isAbsolute(rel));
    }
    
    // Resuelve el último ancestro existente: Add File también puede escribir a través de una junction.
    function physical(target) {
      let cursor = resolve(target);
      const tail = [];
      while (!existsSync(cursor)) {
        const parent = dirname(cursor);
        if (parent === cursor) return resolve(target);
        tail.unshift(basename(cursor));
        cursor = parent;
      }
      try { return resolve(realpathSync.native(cursor), ...tail); }
      catch { return resolve(target); }
    }
    
    function exchangeRoot(root) {
      for (let dir = resolve(root);;) {
        const candidate = resolve(dir, "intercambio");
        try {
          if (existsSync(candidate) && statSync(candidate).isDirectory()) return candidate;
        } catch {}
        const parent = dirname(dir);
        if (parent === dir) return null;
        dir = parent;
      }
    }
    
    // Un proyecto vive en <área>/proyectos/<nombre>/ y hereda de <área>/lore/ — convención
    // documentada en bot-desarrollo-web/CLAUDE.md y en la skill save-to-lore ("A project lives
    // in {area}/proyectos/{name}/ and inherits from {area}/lore/"), no algo local a un bot.
    // Si la sesión abrió dentro de un proyecto con esa forma, el área es jurisdicción propia
    // -escribir criterio confirmado y genérico ahí arriba es la operación que la skill nombra
    // como CAPTURE con promoción-, no un árbol hermano que necesite intercambio/.
    // NC-A-2 (2026-09-21): la sesión abrió en desarrollo-web/proyectos/numerologia y el bloqueo
    // cayó sobre desarrollo-web/lore/mecanica.md -- federatedRoots no alcanza este caso porque
    // lee <root>/lore/enrutamiento.md, y un proyecto de sitio no tiene esa tabla (vive en el
    // bot que federa el área, un árbol primo, no un ancestro).
    function areaRoot(root) {
      const own = resolve(root);
      const parent = dirname(own);
      if (basename(parent) !== "proyectos") return null;
      const area = dirname(parent);
      try {
        if (existsSync(resolve(area, "lore")) && statSync(resolve(area, "lore")).isDirectory()) return resolve(area, "lore");
      } catch {}
      return null;
    }
    
    // Árboles hermanos que el enrutamiento del bot declara (celda con acento grave en las
    // filas de tabla de lore/enrutamiento.md). Cada celda es una ruta relativa a algún
    // ancestro de la raíz: se resuelve subiendo y gana el primer directorio que exista.
    export function federatedRoots(root) {
      const table = resolve(root, "lore", "enrutamiento.md");
      if (!existsSync(table)) return [];
      let text;
      try { text = readFileSync(table, "utf8"); } catch { return []; }
      const cells = new Set();
      for (const line of text.split(/\r?\n/)) {
        if (!line.trimStart().startsWith("|")) continue;
        for (const m of line.matchAll(/\`([^`]+)\`/g)) cells.add(m[1].trim());
      }
      const found = [];
      for (const cell of cells) {
        for (let dir = resolve(root);;) {
          const candidate = resolve(dir, cell);
          try {
            if (existsSync(candidate) && statSync(candidate).isDirectory() && candidate !== resolve(root)) { found.push(candidate); break; }
          } catch {}
          const parent = dirname(dir);
          if (parent === dir) break;
          dir = parent;
        }
      }
      return found;
    }
    
    // `input = {}` solo cubre `undefined`: un `tool_input: null` (o array, o escalar) hacía crash.
    // RC6: se degrada a sin rutas, que es el contrato declarado al pie del archivo — fallar abierto.
    export function structuredWritePaths(toolName, input = {}) {
      const payload = input ?? {};
      if (["Write", "Edit", "MultiEdit"].includes(toolName) && typeof payload.file_path === "string") {
        return [payload.file_path];
      }
      if (toolName === "NotebookEdit" && typeof payload.notebook_path === "string") {
        return [payload.notebook_path];
      }
      if (toolName === "apply_patch" && typeof payload.command === "string") {
        return [...payload.command.matchAll(/^\*\*\* (?:(?:Add|Update|Delete) File|Move to): (.+)$/gm)]
          .map((match) => match[1].trim());
      }
      return [];
    }
    
    // R16 (RC4): la guardia deja de decidir por una lista de permitidos. Cada destino es propio (pasa),
    // ajeno (se bloquea: criterio de otro dueño) o desconocido (pasa con aviso y constancia).
    // El diseño está en el bot que escribe este kit, en `specs/012-rc4/diseno-guardia-r16.md`: el archivo
    // va con el paquete y lo lee cualquiera que lo instale, así que no lleva la ruta del árbol
    // donde se escribió.
    
    // El nombre que Claude Code da al proyecto de una ruta: ~/.claude/projects/<slug>/, donde
    // todo lo que no sea letra ni dígito ASCII pasa a `-`, sin colapsar (`C:` + `\` se vuelven
    // `C--`, y una `ñ` se vuelve `-` como cualquier otro carácter). No es una convención de este
    // kit: se leyó de 42 proyectos reales de la máquina donde se escribió, con `:`, `\`, `-` y `ñ`
    // entre los caracteres que aparecen, y cero discrepancias. La comparación es EXACTA a
    // propósito —la forma de una ruta no concede jurisdicción— y todo lo que no coincida cae en
    // `unknown`, que pasa con aviso: el error posible es un aviso de más, nunca un bloqueo.
    function claudeProjectSlug(root) {
      return resolve(root).replace(/[^A-Za-z0-9]/g, "-");
    }
    
    // La memoria de una raíz propia es propia: es el camino que NC-B-2 abrió (tres veces en uso
    // real, y la tercera decidió que se corrigiera para siempre) y no puede volver a cerrarse.
    // La lista se deriva de las raíces que YA eran propias, así que la autoridad viene de la raíz
    // de sesión y no del nombre de una carpeta. Antes esto era un caso especial que miraba la
    // FORMA de la ruta —«dentro de ~/.claude/projects y con `memory` en el segundo nivel»—, y con
    // eso la memoria de cualquier proyecto de cualquier 
  • hooks/lore-state.mjsGitHub
    Read the script
    // Estado del Lore de un árbol, por contenido — Lore Plugin.
    //
    // El único piso común que tienen los dos hosts y todos los usuarios es el sistema
    // de archivos. No `git` (no es requisito del kit, y hay árboles de Lore sin él),
    // no el transcript (invisible a las escrituras por script), no lo que el agente
    // diga haber corrido (una frase no es un hecho).
    //
    // Se usa desde la guardia de Codex y desde los subcomandos locales de `lore-plugin mycelium`.
    
    import { createHash, randomUUID } from "node:crypto";
    import {
      chmodSync,
      existsSync,
      lstatSync,
      mkdirSync,
      readdirSync,
      readFileSync,
      renameSync,
      statSync,
      unlinkSync,
      writeFileSync,
    } from "node:fs";
    import { tmpdir } from "node:os";
    import { basename, isAbsolute, join, relative, resolve, sep } from "node:path";
    
    // Un archivo de Lore: un `.md` dentro de un `lore/`, o uno de los nombres
    // distintivos de criterio en cualquier nivel. FASES/PHASES es estado y nunca entra.
    const LORE_DIR = /(^|[/\\])lore[/\\][^/\\]+\.md$/i;
    const LORE_FILE = /(^|[/\\])(principios|principles|identidad|identity|enrutamiento|routing)\.md$/i;
    const PHASE_FILE = /(^|[/\\])(FASES|PHASES)\.md$/i;
    
    // Directorios universales de dependencias y artefactos, más las dos formas en que
    // un árbol contiene Lore que no es suyo: **fixtures** (dato de prueba) y **backups**
    // (una copia de otro árbol). El principio detrás de las dos es el mismo — *una copia
    // de un Lore no es el Lore del árbol que la contiene* — y la frontera es honesta: el
    // recorrido no puede distinguir una copia cualquiera de un original, así que se apoya
    // en convenciones de nombre. Un árbol que guarde copias con otro nombre las verá
    // contadas, y su digest se moverá cuando esas copias se regeneren.
    //
    // No se listan nombres de carpeta propios de ningún ecosistema: generalizar desde un
    // solo caso es cómo una forma se lleva puesto lo que era propio de ese caso.
    const SKIP = new Set([
      "node_modules", ".git", ".venv", "venv", "dist", "build", "coverage",
      ".next", "__pycache__",
      "fixtures", "__fixtures__", "test-fixtures",
      "_backup", "backup", "backups", ".backup",
    ]);
    
    const MAX_DEPTH = 6;
    const MAX_BYTES = 2_000_000;
    
    export const RECEIPT = ".lore-mycelium";
    
    // El receipt distingue entre escaneo estructural (bodies/federated) y escaneo completo
    // (cada pista con su paso). El cierre de MYCELIUM no se da por hecho tras correr solo
    // los escaneos estructurales: el receipt registra qué tipo de escaneo se hizo.
    export const SWEEP_TYPES = ["structural", "full"];
    export const SWEEP_TYPE_DEFAULT = "structural";
    
    export function loreFiles(root, { maxDepth = MAX_DEPTH } = {}) {
      const found = [];
      const walk = (dir, depth) => {
        if (depth > maxDepth) return;
        let entries;
        try {
          entries = readdirSync(dir, { withFileTypes: true });
        } catch {
          return;
        }
        for (const e of entries) {
          const full = join(dir, e.name);
          if (e.isDirectory()) {
            if (!SKIP.has(e.name)) walk(full, depth + 1);
          } else if (e.isFile() && !PHASE_FILE.test(full)
            && (LORE_DIR.test(full) || LORE_FILE.test(full))) {
            found.push(full);
          }
        }
      };
      walk(root, 0);
      return found.sort();
    }
    
    function normalizedBody(file) {
      return readFileSync(file, "utf8").replace(/\r\n/g, "\n");
    }
    
    function loreBodies(files) {
      const bodies = new Map();
      for (const file of files) {
        try {
          if (statSync(file).size <= MAX_BYTES) bodies.set(file, normalizedBody(file));
        } catch {}
      }
      return bodies;
    }
    
    function digestBodies(root, files, bodies) {
      const h = createHash("sha256");
      for (const file of files) {
        const body = bodies.get(file);
        if (body === undefined) continue;
        h.update(relative(root, file).split(sep).join("/"));
        h.update("\0");
        h.update(createHash("sha256").update(body).digest("hex"));
        h.update("\n");
      }
      return h.digest("hex");
    }
    
    // Digest por CONTENIDO, no por mtime: tocar un archivo sin cambiarlo no cuenta,
    // y el mismo árbol da el mismo digest en otra máquina.
    export function digest(root) {
      const files = loreFiles(root);
      return digestBodies(root, files, loreBodies(files));
    }
    
    const CONTRACTS = ["CLAUDE.md", "AGENTS.md"];
    const BLOCK = /<!--\s*lore:always-on\s*-->([\s\S]*?)<!--\s*\/lore:always-on\s*-->/;
    
    function alwaysOnFiles(root) {
      const contract = CONTRACTS.find((name) => existsSync(join(root, name)));
      if (!contract) return [];
    
      let scope;
      try {
        scope = BLOCK.exec(readFileSync(join(root, contract), "utf8"))?.[1];
      } catch {
        return [];
      }
      if (!scope) return [];
    
      const found = new Set();
      for (const match of scope.matchAll(/`([^`]+)`/g)) {
        const pointer = match[1].trim();
        const segments = pointer.split(/[/\\]+/);
        if (isAbsolute(pointer)
          || !pointer.toLowerCase().endsWith(".md")
          || !segments.some((part) => /^(lore|canon)$/i.test(part))
          || /^(FASES|PHASES)\.md$/i.test(basename(pointer))) continue;
    
        const file = resolve(root, pointer);
        try {
          if (statSync(file).isFile()) found.add(file);
        } catch {}
      }
      return [...found];
    }
    
    export function snapshot(root) {
      const files = loreFiles(root);
      const bodies = loreBodies(files);
      return {
        digest: digestBodies(root, files, bodies),
        fileCount: files.length,
        alwaysOnBytes: alwaysOnFiles(root).reduce((sum, file) =>
          sum + Buffer.byteLength(bodies.get(file) ?? normalizedBody(file)), 0),
      };
    }
    
    export function readReceipt(root) {
      try {
        const raw = readFileSync(join(root, RECEIPT), "utf8").trim();
        if (/^[0-9a-f]{64}$/.test(raw)) {
          return { version: 1, digest: raw, alwaysOnBytes: null };
        }
        const receipt = JSON.parse(raw);
        if (receipt?.version !== 2
          || !/^[0-9a-f]{64}$/.test(receipt.digest)
          || !Number.isInteger(receipt.alwaysOnBytes)
          || receipt.alwaysOnBytes < 0) return null;
    
        const state = {
          version: 2,
          digest: receipt.digest,
          alwaysOnBytes: receipt.alwaysOnBytes,
          sweepType: SWEEP_TYPES.includes
  • hooks/lore-turno.mjsGitHub
  • hooks/openchamber-plugin.mjsGitHub
  • hooks/opencode-input.mjsGitHub
  • hooks/opencode-plugin.jsGitHub
  • hooks/statusline.mjsGitHub

All 11 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.

@andresanemic
Ships withlore

Stop explaining your project to the AI every morning. With your approval, Lore turns lessons into project criteria the AI can use next time. Provider-neutral and local-first.

79 skills1 agent
Get the whole plugin

Stars on GitHub

Momentum

stars in total
7
Star history of andresanemic/lore-plugin on GitHub, a chart by star-history.com

More plugins that ship hooks.

superpowers

superpowers

A development methodology for coding agents that makes the agent brainstorm before building, follow plans, run parallel work and finish branches in a disciplined way. You get fewer skipped steps and more consistent results from your coding assistant.

15 skills1 hook318k
@obra@obra
Install Plugin
ponytail

ponytail

A Claude Code plugin with skills for auditing a whole repo, reviewing a change, listing deferred shortcut comments, and showing measured savings and a quick command reference.

6 skills10 hooks191k
@dietrichgebert@dietrichgebert
Install Plugin
agent-skills

agent-skills

Walks a software change from a written spec through design, testing, code review, CI/CD, and release, applying the matching engineering skill at each stage. Useful for developers using Claude Code on full development workflows.

25 skills+3117k
@addyosmani@addyosmani
Install Plugin
planning-with-files

planning-with-files

Persistent file-based planning for AI coding agents on long-running, multi-step tasks. It keeps task_plan.md, findings.md and progress.md on disk, so work can resume after /clear or context compaction.

6 skills+228k
@othmanadi@othmanadi
Install Plugin
gsd-core

gsd-core

A spec-driven development workflow for Claude Code that guides a project through planning, phases, milestone audits and fixes. Includes skills to generate tests from UAT criteria and to audit completed work.

72 skills+312k
@open-gsd@open-gsd
Install Plugin
superpowers-zh

superpowers-zh

Chinese-language skill pack for Claude Code that adds workflow guidance for brainstorming, TDD, debugging, and code review, plus Chinese conventions for commits, documentation, and domestic Git platforms like Gitee and Coding.net.

21 skills1 hook8.5k
@jnmetacode@jnmetacode
Install Plugin
socraticode

socraticode

Gives Claude Code semantic search, dependency graph and architecture context over a codebase, so an AI assistant can explore and understand a project's code. Includes skills for indexing a project and managing its file watching.

2 skills+33.4k
@giancarloerra@giancarloerra
Install Plugin
cocoindex-code

cocoindex-code

A lightweight code search engine that uses AST-based semantic search with local embeddings and incremental indexing. It helps Claude Code users find code faster and more efficiently.

1 skill1 MCP+13k
@cocoindex-io@cocoindex-io
Install Plugin

Questions about the hooks in lore.

Which coding agents can use the hooks in lore?
The hooks in lore install in Claude Code and Cursor: each runs Claude Code hooks from .claude/settings.json, so copy the hooks there.