Skip to content
Development
Hook

Hooks

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

From plugin
context-mode
20k11 skills6 hooks1 MCP
Install
> /plugin marketplace add mksglu/context-mode
> /plugin install context-mode@context-mode

Ships with context-mode. Installing the plugin gets these hooks.

What fires, and when

PostToolUse

  • MatchesBash|Read|Write|Edit|NotebookEdit|Glob|Grep|TodoWrite|TaskCreatenode "${CLAUDE_PLUGIN_ROOT}/hooks/posttooluse.mjs"

PreCompact

  • node "${CLAUDE_PLUGIN_ROOT}/hooks/precompact.mjs"

PreToolUse

  • MatchesBashnode "${CLAUDE_PLUGIN_ROOT}/hooks/pretooluse.mjs"
  • MatchesWebFetchnode "${CLAUDE_PLUGIN_ROOT}/hooks/pretooluse.mjs"
  • MatchesReadnode "${CLAUDE_PLUGIN_ROOT}/hooks/pretooluse.mjs"
  • MatchesGrepnode "${CLAUDE_PLUGIN_ROOT}/hooks/pretooluse.mjs"
  • MatchesAgentnode "${CLAUDE_PLUGIN_ROOT}/hooks/pretooluse.mjs"
  • Matchesmcp__plugin_context-mode_context-mode__ctx_executenode "${CLAUDE_PLUGIN_ROOT}/hooks/pretooluse.mjs"
  • Matchesmcp__plugin_context-mode_context-mode__ctx_execute_filenode "${CLAUDE_PLUGIN_ROOT}/hooks/pretooluse.mjs"
  • Matchesmcp__plugin_context-mode_context-mode__ctx_batch_executenode "${CLAUDE_PLUGIN_ROOT}/hooks/pretooluse.mjs"
  • Matchesmcp__node "${CLAUDE_PLUGIN_ROOT}/hooks/pretooluse.mjs"

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.

  • node "${CLAUDE_PLUGIN_ROOT}/hooks/userpromptsubmit.mjs"

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/sessionstart.mjs"

Stop

  • node "${CLAUDE_PLUGIN_ROOT}/hooks/stop.mjs"
Read hooks/hooks.json

In the plugin's words

How context-mode describes its own hook set.

Context-mode hooks — PreToolUse routing, PostToolUse session capture, UserPromptSubmit decisions, PreCompact snapshot, SessionStart context injection, Stop turn-end capture

Where it lives

  • hooks/auto-injection.mjsGitHub
  • hooks/cache-heal-utils.mjsGitHub
  • hooks/ensure-deps.mjsGitHub
  • hooks/heal-partial-install.mjsGitHub
  • hooks/normalize-hooks.mjsGitHub
  • hooks/platform-bridge.mjsGitHub
  • hooks/posttooluse.mjsRunsGitHub
    Read the script
    #!/usr/bin/env node
    /**
     * PostToolUse hook for context-mode session continuity.
     *
     * Captures session events from tool calls (13 categories) and stores
     * them in the per-project SessionDB for later resume snapshot building.
     *
     * Must be fast (<20ms). No network, no LLM, just SQLite writes.
     *
     * Crash-resilience: wrapped via runHook (#414).
     */
    
    import { runHook } from "./run-hook.mjs";
    
    await runHook(async () => {
      const {
        readStdin,
        parseStdin,
        getSessionId,
        getSessionDBPath,
        getInputProjectDir,
      } = await import("./session-helpers.mjs");
      const { createSessionLoaders, attributeAndInsertEvents } = await import("./session-loaders.mjs");
      const { dirname, resolve, basename } = await import("node:path");
      const { fileURLToPath } = await import("node:url");
      const { readFileSync, unlinkSync } = await import("node:fs");
      const { tmpdir } = await import("node:os");
    
      // Resolve absolute path for imports — relative dynamic imports can fail
      // when Claude Code invokes hooks from a different working directory.
      const HOOK_DIR = dirname(fileURLToPath(import.meta.url));
      const { loadSessionDB, loadExtract, loadProjectAttribution } = createSessionLoaders(HOOK_DIR);
    
      try {
        const raw = await readStdin();
        const input = parseStdin(raw);
        const projectDir = getInputProjectDir(input);
    
        const { extractEvents } = await loadExtract();
        const { resolveProjectAttributions } = await loadProjectAttribution();
        const { SessionDB } = await loadSessionDB();
    
        const dbPath = getSessionDBPath();
        const db = new SessionDB({ dbPath });
        const sessionId = getSessionId(input);
    
        // Ensure session meta exists
        db.ensureSession(sessionId, projectDir);
    
        // Extract and store events
        const events = extractEvents({
          tool_name: input.tool_name,
          tool_input: input.tool_input ?? {},
          tool_response: typeof input.tool_response === "string"
            ? input.tool_response
            : JSON.stringify(input.tool_response ?? ""),
          tool_output: input.tool_output,
        });
    
        attributeAndInsertEvents(db, sessionId, events, input, projectDir, "PostToolUse", resolveProjectAttributions);
    
        // ─── Category 18: Rejected-approach — read PreToolUse marker ───
        try {
          const rejectedPath = resolve(tmpdir(), `context-mode-rejected-${sessionId}.txt`);
          let rejectedData;
          try {
            rejectedData = readFileSync(rejectedPath, "utf-8").trim();
            unlinkSync(rejectedPath);
          } catch { /* no marker */ }
          if (rejectedData) {
            const colonIdx = rejectedData.indexOf(":");
            const rejTool = colonIdx > 0 ? rejectedData.slice(0, colonIdx) : rejectedData;
            const rejReason = colonIdx > 0 ? rejectedData.slice(colonIdx + 1) : "denied";
            // v1.0.160: route through attributeAndInsertEvents so the bridge wire
            // receives this event too. db.insertEvent only writes locally — the
            // dashboard's rejection-rate widget needs the platform row.
            attributeAndInsertEvents(
              db,
              sessionId,
              [{
                type: "rejected",
                category: "rejected-approach",
                data: `${rejTool}: ${rejReason}`,
                priority: 2,
              }],
              input,
              projectDir,
              "PreToolUse",
              resolveProjectAttributions,
            );
          }
        } catch { /* best-effort */ }
    
        // ─── D2 PRD Phase 3/4: redirect marker — emit byte-accounting event ───
        // PreToolUse wrote `context-mode-redirect-${sessionId}.txt` for tools whose
        // output we kept out of the model's context window (curl/wget, WebFetch,
        // large Read). Format: `tool:type:bytesAvoided:commandSummary` (Override C).
        try {
          const redirectPath = resolve(tmpdir(), `context-mode-redirect-${sessionId}.txt`);
          let redirectData;
          try {
            redirectData = readFileSync(redirectPath, "utf-8").trim();
            // Slice 3.3: unlink so the next PostToolUse for an unrelated tool call
            // does NOT re-emit the same event (no double-accounting).
            unlinkSync(redirectPath);
          } catch { /* no marker — Slice 3.4: phantom-event guard */ }
    
          if (redirectData) {
            // Parse first 3 colons; the rest (commandSummary) may itself contain
            // colons (URLs do — `https://`). Avoid `split(":", 4)` which would
            // truncate the summary at any embedded colon.
            const i1 = redirectData.indexOf(":");
            const i2 = i1 >= 0 ? redirectData.indexOf(":", i1 + 1) : -1;
            const i3 = i2 >= 0 ? redirectData.indexOf(":", i2 + 1) : -1;
            if (i1 > 0 && i2 > i1 && i3 > i2) {
              const tool = redirectData.slice(0, i1);
              const type = redirectData.slice(i1 + 1, i2);
              const bytesRaw = redirectData.slice(i2 + 1, i3);
              const summary = redirectData.slice(i3 + 1);
              const bytesAvoided = Number.parseInt(bytesRaw, 10);
              if (Number.isFinite(bytesAvoided) && bytesAvoided > 0) {
                // v1.0.160: route through wire — context-saving (byte-accounting)
                // widget on the platform reads category='redirect' rows. event
                // carries bytes_avoided so the bytesList branch in
                // attributeAndInsertEvents stamps the column.
                attributeAndInsertEvents(
                  db,
                  sessionId,
                  [{
                    type,
                    category: "redirect",
                    data: `${tool}: ${summary}`,
                    priority: 2,
                    bytes_avoided: bytesAvoided,
                  }],
                  input,
                  projectDir,
                  "PreToolUse",
                  resolveProjectAttributions,
                );
              }
            }
          }
        } catch { /* best-effort — never block hook */ }
    
        // ─── Category 27: Latency — read cross-hook marker and emit event if slow ───
        try {
          const toolName = input.tool_name ?? "";
          if (toolName) {
            const markerPath = resolve(tmpdir(), `context-mode-latency-${sessionId}-${toolNa
  • hooks/precompact.mjsRunsGitHub
    Read the script
    #!/usr/bin/env node
    /**
     * PreCompact hook for context-mode session continuity.
     *
     * Triggered when Claude Code is about to compact the conversation.
     * Reads all captured session events, builds a priority-sorted resume
     * snapshot (<2KB XML), and stores it for injection after compact.
     *
     * Crash-resilience: wrapped via runHook (#414).
     */
    
    import { runHook } from "./run-hook.mjs";
    
    await runHook(async () => {
      const {
        readStdin,
        parseStdin,
        getSessionId,
        getSessionDBPath,
        getInputProjectDir,
        resolveConfigDir,
      } = await import("./session-helpers.mjs");
      const { createSessionLoaders, attributeAndInsertEvents } = await import("./session-loaders.mjs");
      const { appendFileSync } = await import("node:fs");
      const { join, dirname } = await import("node:path");
      const { fileURLToPath } = await import("node:url");
    
      // Resolve absolute path for imports
      const HOOK_DIR = dirname(fileURLToPath(import.meta.url));
      const { loadSessionDB, loadSnapshot, loadProjectAttribution } = createSessionLoaders(HOOK_DIR);
      const DEBUG_LOG = join(resolveConfigDir(), "context-mode", "precompact-debug.log");
    
      try {
        const raw = await readStdin();
        const input = parseStdin(raw);
    
        const { buildResumeSnapshot } = await loadSnapshot();
        const { SessionDB } = await loadSessionDB();
    
        const dbPath = getSessionDBPath();
        const db = new SessionDB({ dbPath });
        const sessionId = getSessionId(input);
    
        // Get all events for this session
        const events = db.getEvents(sessionId);
    
        if (events.length > 0) {
          const stats = db.getSessionStats(sessionId);
          const snapshot = buildResumeSnapshot(events, {
            compactCount: (stats?.compact_count ?? 0) + 1,
          });
    
          db.upsertResume(sessionId, snapshot, events.length);
          db.incrementCompactCount(sessionId);
    
          // v1.0.160: route compaction lifecycle events through wire so
          // dashboard's compact widget gets per-compaction rows (the engine
          // joins on category='compaction' to compute snapshot insights).
          try {
            const fileEvents = events.filter(e => e.category === "file");
            const projectDirCompact = getInputProjectDir(input);
            const { resolveProjectAttributions } = await loadProjectAttribution();
            attributeAndInsertEvents(
              db,
              sessionId,
              [
                {
                  type: "compaction_summary",
                  category: "compaction",
                  data: `Session compacted. ${events.length} events, ${fileEvents.length} files touched.`,
                  priority: 1,
                },
                {
                  type: "snapshot-built",
                  category: "compaction",
                  data: `Snapshot built. ${snapshot.length} bytes for ${events.length} events.`,
                  priority: 1,
                  bytes_avoided: snapshot.length,
                },
              ],
              input,
              projectDirCompact,
              "PreCompact",
              resolveProjectAttributions,
            );
          } catch { /* best-effort — never block PreCompact */ }
        }
    
        db.close();
      } catch (err) {
        try {
          appendFileSync(DEBUG_LOG, `[${new Date().toISOString()}] ${err.message}\n`);
        } catch {
          // Silent fallback
        }
      }
    
      // PreCompact doesn't need hookSpecificOutput
      console.log(JSON.stringify({}));
    });
    
  • hooks/pretooluse.mjsRunsGitHub
    Read the script
    #!/usr/bin/env node
    /**
     * Unified PreToolUse hook for context-mode (Claude Code)
     * Redirects data-fetching tools to context-mode MCP tools
     *
     * Cross-platform (Windows/macOS/Linux) — no bash/jq dependency.
     *
     * Routing is delegated to core/routing.mjs (shared across platforms).
     * This file retains the Claude Code-specific self-heal block and
     * uses core/formatters.mjs for Claude Code output format.
     *
     * Crash-resilience: wrapped via runHook (#414) — module loads happen
     * dynamically inside the wrapper.
     *
     * #415: the destructive settings.json mutation block (which removed
     * context-mode hook entries when hooks.json was present) was deleted.
     * It deleted user-written hook configs without consent and was the
     * documented cause of the regression.
     */
    
    import { runHook } from "./run-hook.mjs";
    
    await runHook(async () => {
      const { readFileSync, writeFileSync, existsSync, mkdirSync, copyFileSync, readdirSync } = await import("node:fs");
      const { resolve, dirname, basename } = await import("node:path");
      const { fileURLToPath } = await import("node:url");
      const { tmpdir } = await import("node:os");
      const { readStdin } = await import("./core/stdin.mjs");
      const { routePreToolUse, initSecurity } = await import("./core/routing.mjs");
      const { formatDecision } = await import("./core/formatters.mjs");
      const { parseStdin, getInputProjectDir, getSessionId, resolveConfigDir } = await import("./session-helpers.mjs");
    
      // ─── Manual recursive copy (avoids cpSync libuv crash on non-ASCII paths, Windows + Node 24) ───
      function copyDirSync(src, dest) {
        mkdirSync(dest, { recursive: true });
        for (const entry of readdirSync(src, { withFileTypes: true })) {
          const srcPath = resolve(src, entry.name);
          const destPath = resolve(dest, entry.name);
          if (entry.isDirectory()) copyDirSync(srcPath, destPath);
          else copyFileSync(srcPath, destPath);
        }
      }
    
      // ─── Self-heal: rename dir to correct version, fix registry + hooks ───
      try {
        const hookDir = dirname(fileURLToPath(import.meta.url));
        const myRoot = resolve(hookDir, "..");
        const myPkg = JSON.parse(readFileSync(resolve(myRoot, "package.json"), "utf-8"));
        const myVersion = myPkg.version ?? "unknown";
        const myDirName = basename(myRoot);
        const cacheParent = dirname(myRoot);
        const marker = resolve(tmpdir(), `context-mode-healed-${myVersion}`);
    
        // Only self-heal inside plugin cache dirs — skip in dev/CI environments
        const isInPluginCache = myRoot.includes("/plugins/cache/") || myRoot.includes("\\plugins\\cache\\");
        if (myVersion !== "unknown" && isInPluginCache && !existsSync(marker)) {
          // 1. If dir name doesn't match version (e.g. "0.7.0" but code is "0.9.12"),
          //    create correct dir, copy files, update registry + hooks
          const correctDir = resolve(cacheParent, myVersion);
          if (myDirName !== myVersion && !existsSync(correctDir)) {
            copyDirSync(myRoot, correctDir);
    
            // Create start.mjs in new dir if missing
            const startMjs = resolve(correctDir, "start.mjs");
            if (!existsSync(startMjs)) {
              writeFileSync(startMjs, [
                '#!/usr/bin/env node',
                'import { existsSync } from "node:fs";',
                'import { dirname, resolve } from "node:path";',
                'import { fileURLToPath } from "node:url";',
                'const __dirname = dirname(fileURLToPath(import.meta.url));',
                'process.chdir(__dirname);',
                'if (!process.env.CLAUDE_PROJECT_DIR) process.env.CLAUDE_PROJECT_DIR = process.cwd();',
                'if (existsSync(resolve(__dirname, "server.bundle.mjs"))) {',
                '  await import("./server.bundle.mjs");',
                '} else if (existsSync(resolve(__dirname, "build", "server.js"))) {',
                '  await import("./build/server.js");',
                '}',
              ].join("\n"), "utf-8");
            }
          }
    
          const targetDir = existsSync(correctDir) ? correctDir : myRoot;
    
          // 2. Update installed_plugins.json → point to correct version dir
          //    Skip if not present (e.g. CI / non-Claude-Code environments)
          const ipPath = resolve(resolveConfigDir(), "plugins", "installed_plugins.json");
          if (existsSync(ipPath)) {
            const ip = JSON.parse(readFileSync(ipPath, "utf-8"));
            for (const [key, entries] of Object.entries(ip.plugins || {})) {
              if (!key.toLowerCase().includes("context-mode")) continue;
              for (const entry of entries) {
                entry.installPath = targetDir;
                entry.version = myVersion;
                entry.lastUpdated = new Date().toISOString();
              }
            }
            writeFileSync(ipPath, JSON.stringify(ip, null, 2) + "\n", "utf-8");
          }
    
          // 3. Legacy: hooks.json absent — rewrite stale paths in settings.json to current version dir.
          //    The previous "if hooks.json present, delete settings.json entries" block was REMOVED (#415):
          //    it destroyed user-written hook configs without consent. Plugin-system + settings.json
          //    coexistence is now Claude Code's responsibility, not ours.
          const settingsPath = resolve(resolveConfigDir(), "settings.json");
          try {
            const settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
            const allHooks = settings.hooks || {};
            let changed = false;
    
            const hooksJsonPath = resolve(myRoot, "hooks", "hooks.json");
            if (!existsSync(hooksJsonPath)) {
              // Legacy: hooks.json absent — rewrite stale paths to current version dir.
              for (const hookType of Object.keys(allHooks)) {
                const entries = allHooks[hookType];
                if (!Array.isArray(entries)) continue;
    
                for (const entry of entries) {
                  // Fix deprecated Task-only matcher (PreToolUse only)
                  if (hookType === "PreToolUse" && entry.matcher?.includes("Task") && !entry.matcher.includes("Agent")) {
                    entry.matcher = entry.matcher.replace("Task", "Agent|Task"
  • hooks/routing-block.mjsGitHub
  • hooks/run-hook.mjsGitHub
  • hooks/security.bundle.mjsGitHub
  • hooks/session-attribution.bundle.mjsGitHub
  • hooks/session-db.bundle.mjsGitHub
  • hooks/session-directive.mjsGitHub
  • hooks/session-extract.bundle.mjsGitHub
  • hooks/session-helpers.mjsGitHub
  • hooks/session-loaders.mjsGitHub
  • hooks/session-snapshot.bundle.mjsGitHub
  • hooks/sessionstart.mjsRunsGitHub
    Read the script
    #!/usr/bin/env node
    /**
     * SessionStart hook for context-mode
     *
     * Provides the agent with XML-structured "Rules of Engagement"
     * at the beginning of each session. Injects session knowledge on
     * both startup and compact to maintain continuity.
     *
     * Session Lifecycle Rules:
     * - "startup"  → Fresh session. Inject previous session knowledge. Cleanup old data.
     * - "compact"  → Auto-compact triggered. Inject resume snapshot + stats.
     * - "resume"   → User invoked --continue, --resume, or /resume. CC sends the
     *                ACTIVE session_id; for /resume this is typically a *fresh*
     *                id, so live events miss → fall back to snapshot (#413).
     * - "clear"    → User cleared context. No resume.
     *
     * Crash-resilience: wrapped via runHook (#414) — all module loads happen
     * dynamically inside the wrapper so a missing/poisoned dep can never hard-fail
     * the hook. Errors land in ~/.claude/context-mode/hook-errors.log.
     */
    
    import { runHook } from "./run-hook.mjs";
    
    await runHook(async () => {
      const { createRoutingBlock } = await import("./routing-block.mjs");
      const { createToolNamer } = await import("./core/tool-naming.mjs");
      const { detectPlatformFromEnv } = await import("./core/platform-detect.mjs");
      const { buildAutoInjection } = await import("./auto-injection.mjs");
      const {
        readStdin,
        parseStdin,
        getSessionId,
        getInputProjectDir,
        getSessionDBPath,
        getSessionEventsPath,
        getCleanupFlagPath,
        resolveConfigDir,
      } = await import("./session-helpers.mjs");
      const { writeSessionEventsFile, buildSessionDirective, getSessionEvents } = await import(
        "./session-directive.mjs"
      );
      const { createSessionLoaders, attributeAndInsertEvents } = await import("./session-loaders.mjs");
      const { join, dirname } = await import("node:path");
      const { fileURLToPath } = await import("node:url");
      const { readFileSync, unlinkSync, readdirSync, rmSync, lstatSync, realpathSync, symlinkSync } = await import("node:fs");
    
      const detectedPlatform = detectPlatformFromEnv();
      const toolNamer = createToolNamer(detectedPlatform);
      const ROUTING_BLOCK = createRoutingBlock(toolNamer);
    
      // Resolve absolute path for imports (fileURLToPath for Windows compat)
      const HOOK_DIR = dirname(fileURLToPath(import.meta.url));
      const { loadSessionDB, loadProjectAttribution, loadExtract } = createSessionLoaders(HOOK_DIR);
    
      // Emit a `session_start` canonical event at the boundary of each session
      // lifecycle transition (startup / resume / compact). The platform's insight
      // engine joins on `category='session_start'` to compute per-session
      // aggregates (~60 of 180 patterns depend on this anchor row). Bridge
      // forwards via attributeAndInsertEvents which also stamps the rollup
      // snapshot — safe for the FIRST event of a fresh session.
      async function emitSessionStartLifecycle(db, sessionId, source, projectDir, input) {
        try {
          const { resolveProjectAttributions } = await loadProjectAttribution();
          const lifecycleEvent = {
            type: "session_start",
            category: "session_start",
            data: JSON.stringify({
              source,
              project_dir: projectDir,
              started_at: Math.floor(Date.now() / 1000),
            }),
            priority: 1,
          };
    
          // PRD #4 — emit session_settings_snapshot alongside lifecycle when
          // the SessionStart envelope carries any of mcp_servers / model /
          // permission_mode. Best-effort: missing fields → no snapshot.
          const eventsToEmit = [lifecycleEvent];
          try {
            const extract = await loadExtract();
            if (typeof extract.extractSessionSettings === "function") {
              eventsToEmit.push(...extract.extractSessionSettings(input));
            }
          } catch {
            // settings snapshot is opportunistic — never block lifecycle on it
          }
    
          attributeAndInsertEvents(
            db,
            sessionId,
            eventsToEmit,
            input,
            projectDir,
            "SessionStart",
            resolveProjectAttributions,
          );
        } catch {
          // Best-effort — lifecycle emission failure MUST NOT block session start.
        }
      }
    
      // Self-heal a partial plugin cache install before anything else
      // touches the cache dir. The Algo-D4 boot gate and the #604
      // normalize-hooks ratchet both fire from start.mjs, which is one of
      // the files that may be missing in the failure mode; sessionstart.mjs
      // fires from CC's hooks.json wiring regardless of MCP boot status, so
      // it is the reliably-available entry point. See
      // hooks/heal-partial-install.mjs for the full failure-mode description.
      try {
        const { healPartialInstallFromMarketplace } = await import("./heal-partial-install.mjs");
        healPartialInstallFromMarketplace();
      } catch { /* best effort, never block session start */ }
    
      // Issue #710 — Layer 2: self-heal Claude Code's per-session shell snapshots.
      //
      // Claude Code `source`s ~/.claude/shell-snapshots/snapshot-*.sh before every
      // Bash tool call (refs/platforms/claude-code/src/utils/bash/ShellSnapshot.ts:269-336,
      // sourced at bashProvider.ts:166). The snapshot bakes an `export PATH='…'`
      // line with the context-mode `bin/` of the version active at session boot.
      // After /ctx-upgrade deletes the old cache dir, the snapshot still points
      // at it — every Bash call fails with "Plugin directory does not exist"
      // until the session restarts.
      //
      // Layer 1 (cli.ts /ctx-upgrade) rewrites the active session's snapshot
      // mid-upgrade so the in-process session never sees the broken state.
      // Layer 2 (this) catches sessions that started after /ctx-upgrade but
      // whose snapshots somehow missed the rewrite (parallel sessions, killed
      // /ctx-upgrade run, manual cache surgery). Resolves currentVersion from
      // the plugin's own manifest — no env-var dependency, immune to PATH bugs.
      // Best-effort, never blocks session start.
      try {
        const { selfHealShellSnapshots } = await import("./cache-heal-utils.mjs");
        const { reso
  • hooks/stop.mjsRunsGitHub
    Read the script
    #!/usr/bin/env node
    import "./suppress-stderr.mjs";
    import "./ensure-deps.mjs";
    /**
     * Claude Code Stop hook — record turn-end state for continuity.
     *
     * Stop fires when Claude is about to finish the current assistant turn. This is
     * not a true session shutdown event, so record a turn_end marker and never ask
     * Claude to continue.
     */
    
    import { readStdin, parseStdin, getSessionId, getSessionDBPath, getInputProjectDir } from "./session-helpers.mjs";
    import { createSessionLoaders, attributeAndInsertEvents } from "./session-loaders.mjs";
    import { dirname } from "node:path";
    import { fileURLToPath } from "node:url";
    import { readFileSync } from "node:fs";
    
    const HOOK_DIR = dirname(fileURLToPath(import.meta.url));
    const { loadSessionDB, loadExtract, loadProjectAttribution } = createSessionLoaders(HOOK_DIR);
    
    try {
      const raw = await readStdin();
      const input = parseStdin(raw);
      const projectDir = getInputProjectDir(input);
    
      const { SessionDB } = await loadSessionDB();
      const dbPath = getSessionDBPath(undefined, projectDir);
      const db = new SessionDB({ dbPath });
      const sessionId = getSessionId(input);
    
      db.ensureSession(sessionId, projectDir);
      const payload = {
        stop_hook_active: input.stop_hook_active ?? false,
        last_assistant_message: typeof input.last_assistant_message === "string"
          ? input.last_assistant_message.slice(0, 2000)
          : null,
      };
      db.insertEvent(sessionId, {
        type: "turn_end",
        category: "session",
        data: JSON.stringify(payload),
        priority: 1,
      }, "Stop");
    
      // ─── claude-code MAIN-turn cost capture (cursor-aware, no double-count) ───
      // The transcript grows every turn and the forward loop forwards ALL passed
      // events, so we emit ONLY the turns NEW since the last Stop, keyed by a
      // per-session high-water cursor. Each step is best-effort — a hook must
      // never block the session, so a transcript read or extract failure here is
      // swallowed without aborting the turn_end write above.
      try {
        const transcriptPath = typeof input.transcript_path === "string" ? input.transcript_path : null;
        if (transcriptPath) {
          let transcript = null;
          try {
            transcript = readFileSync(transcriptPath, "utf-8");
          } catch {
            // unreadable/missing transcript — skip capture this turn.
          }
          if (transcript) {
            const { extractTranscriptUsageSince } = await loadExtract();
            const { resolveProjectAttributions } = await loadProjectAttribution();
            const cursor = db.getUsageCursor(sessionId);
            const { events, cursor: next } = extractTranscriptUsageSince(transcript, cursor);
            if (events.length > 0) {
              // attributeAndInsertEvents both INSERTS locally and FORWARDS to the
              // platform (gated on ~/.context-mode/platform.json).
              attributeAndInsertEvents(db, sessionId, events, input, projectDir, "Stop", resolveProjectAttributions);
            }
            if (next) db.setUsageCursor(sessionId, next);
          }
        }
      } catch {
        // Best-effort cost capture — never block the session on failure.
      }
    
      db.close();
    } catch {
      // Claude Code hooks must not block the session.
    }
    
    process.stdout.write("{}\n");
    
  • hooks/suppress-stderr.mjsGitHub
  • hooks/userpromptsubmit.mjsRunsGitHub
    Read the script
    #!/usr/bin/env node
    /**
     * UserPromptSubmit hook for context-mode session continuity.
     *
     * Captures every user prompt so the LLM can continue from the exact
     * point where the user left off after compact or session restart.
     *
     * Must be fast (<10ms). Just a single SQLite write.
     *
     * Crash-resilience: wrapped via runHook (#414) — module loads happen
     * dynamically so missing deps log + exit 0 instead of MODULE_NOT_FOUND.
     */
    
    import { runHook } from "./run-hook.mjs";
    
    await runHook(async () => {
      const {
        readStdin,
        parseStdin,
        getSessionId,
        getSessionDBPath,
        getInputProjectDir,
      } = await import("./session-helpers.mjs");
      const { createSessionLoaders, attributeAndInsertEvents } = await import("./session-loaders.mjs");
      const { dirname } = await import("node:path");
      const { fileURLToPath } = await import("node:url");
    
      const HOOK_DIR = dirname(fileURLToPath(import.meta.url));
      const { loadSessionDB, loadExtract, loadProjectAttribution } = createSessionLoaders(HOOK_DIR);
    
      try {
        const raw = await readStdin();
        const input = parseStdin(raw);
        const projectDir = getInputProjectDir(input);
    
        const prompt = input.prompt ?? input.message ?? "";
        const trimmed = (prompt || "").trim();
    
        // Skip system-generated messages — only capture genuine user prompts
        const isSystemMessage = trimmed.startsWith("<task-notification>")
          || trimmed.startsWith("<system-reminder>")
          || trimmed.startsWith("<context_guidance>")
          || trimmed.startsWith("<tool-result>");
    
        if (trimmed.length > 0 && !isSystemMessage) {
          const { SessionDB } = await loadSessionDB();
          const { extractUserEvents, extractUserPromptFeatures } = await loadExtract();
          const { resolveProjectAttributions } = await loadProjectAttribution();
          const dbPath = getSessionDBPath();
          const db = new SessionDB({ dbPath });
          const sessionId = getSessionId(input);
    
          db.ensureSession(sessionId, projectDir);
    
          // 1. Always save the raw prompt with F1 §2 features attached.
          // Features attach to the existing user_prompt event payload alongside
          // the raw `data` field (do NOT remove `data`). Platform Zod envelope
          // is forward-compatible; new fields persist as typed columns.
          const promptFeatures = typeof extractUserPromptFeatures === "function"
            ? extractUserPromptFeatures(trimmed)
            : {};
          const promptEvent = {
            type: "user_prompt",
            category: "user-prompt",
            data: prompt,
            priority: 1,
            ...promptFeatures,
          };
          const promptAttributions = attributeAndInsertEvents(
            db, sessionId, [promptEvent], input, projectDir, "UserPromptSubmit", resolveProjectAttributions,
          );
    
          // 2. Extract decision/role/intent/data from user message
          const userEvents = extractUserEvents(trimmed);
          // Feed lastKnownProjectDir from the first attribution into the second batch
          const savedLastKnown = promptAttributions[0]?.projectDir || null;
          const sessionStats = db.getSessionStats(sessionId);
          const lastKnownProjectDir = typeof db.getLatestAttributedProjectDir === "function"
            ? db.getLatestAttributedProjectDir(sessionId)
            : null;
          const userAttributions = resolveProjectAttributions(userEvents, {
            sessionOriginDir: sessionStats?.project_dir || projectDir,
            inputProjectDir: projectDir,
            workspaceRoots: Array.isArray(input.workspace_roots) ? input.workspace_roots : [],
            lastKnownProjectDir: savedLastKnown || lastKnownProjectDir,
          });
          // v1.0.160: route through wire so prompt-derived events (decision /
           // role / intent / data extractions) reach the platform. Previously
           // they only landed in local SessionDB → dashboard's prompt-flow
           // insights stayed at 0.
          if (userEvents.length > 0) {
            attributeAndInsertEvents(
              db,
              sessionId,
              userEvents,
              input,
              projectDir,
              "UserPromptSubmit",
              resolveProjectAttributions,
            );
          }
    
          db.close();
        }
      } catch {
        // UserPromptSubmit must never block the session — silent fallback
      }
    });
    

All 23 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 withcontext-mode

The other half of the context problem. Used across teams at

Get the whole plugin
Stats
19,748
Stars
1,414
Forks
Active
Maintenance
TypeScript
Language
9h ago
Last commit
5mo ago
Created

Repo: mksglu/context-mode