Skip to content
Development
Hook

Hooks

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

From plugin
stitch-kit
4536 skills1 agent2 hooks
Install
> /plugin marketplace add gabelul/stitch-kit
> /plugin install stitch-kit@stitch-kit

Ships with stitch-kit. Installing the plugin gets these hooks.

What fires, and when

PreCompact

  • Matchesautonode "${CLAUDE_PLUGIN_ROOT}/hooks/pre-compact.mjs"
  • Matchesmanualnode "${CLAUDE_PLUGIN_ROOT}/hooks/pre-compact.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.

  • Matchescompactnode "${CLAUDE_PLUGIN_ROOT}/hooks/session-start.mjs"
  • Matchesresumenode "${CLAUDE_PLUGIN_ROOT}/hooks/session-start.mjs"
Read hooks/hooks.json

Where it lives

  • hooks/pre-compact.mjsRunsGitHub
    Read the script
    #!/usr/bin/env node
    /**
     * pre-compact.mjs — PreCompact hook (matchers: auto, manual)
     *
     * Fires right before the host compacts the conversation. By this point the
     * skills have already been writing state to .stitch/session as they go, so the
     * important stuff is on disk. This hook is the backstop on top of that:
     *
     *   1. Copies the raw transcript into snapshots/ (owner-only perms) so nothing
     *      is ever truly lost. Best-effort and isolated — if the copy fails, the
     *      breadcrumb below still gets written.
     *   2. Refreshes RESUME.md with a human/agent-readable breadcrumb.
     *
     * Two hard rules:
     *   - No active Stitch session → do nothing, exit 0.
     *   - ALWAYS exit 0. PreCompact can block compaction with exit code 2, and the
     *     last thing we want is to wedge someone's session because a copy failed.
     */
    
    import {
      readFileSync,
      copyFileSync,
      chmodSync,
      existsSync,
      mkdirSync,
      writeFileSync,
      readdirSync,
      rmSync,
    } from "node:fs";
    import { join } from "node:path";
    import { loadState, formatStatus, sessionDir, snapshotsDir, resumeFile } from "../scripts/stitch-session.mjs";
    
    /** Keep at most this many transcript snapshots so the backstop can't bloat the project. */
    const MAX_SNAPSHOTS = 5;
    
    /** Delete the oldest snapshots beyond MAX_SNAPSHOTS (lexical sort works — names are timestamped). */
    function pruneSnapshots(dir) {
      try {
        const files = readdirSync(dir)
          .filter((f) => f.startsWith("transcript-") && f.endsWith(".jsonl"))
          .sort();
        for (const stale of files.slice(0, Math.max(0, files.length - MAX_SNAPSHOTS))) {
          rmSync(join(dir, stale), { force: true });
        }
      } catch {
        // pruning is housekeeping — failing it must not affect compaction
      }
    }
    
    try {
      // Parse the hook input (JSON on stdin) FIRST, so we can resolve the project
      // root from input.cwd before touching any state. session_id, transcript_path,
      // trigger, cwd are the fields we use.
      let sessionId = "session";
      let transcriptPath = "";
      let trigger = "auto";
      try {
        const raw = readFileSync(0, "utf8");
        if (raw) {
          const input = JSON.parse(raw);
          if (input.cwd && !process.env.CLAUDE_PROJECT_DIR) process.env.CLAUDE_PROJECT_DIR = input.cwd;
          sessionId = (input.session_id || "session").toString().replace(/[^\w.-]/g, "_");
          transcriptPath = (input.transcript_path || "").toString();
          trigger = (input.trigger || "auto").toString();
        }
      } catch {
        // missing/garbled input — proceed with defaults, still write the breadcrumb
      }
    
      // Only do anything if there's an active Stitch session to protect.
      const state = loadState();
      if (state) {
        mkdirSync(sessionDir(), { recursive: true });
    
        // 1. Raw transcript backstop. Isolated in its own try so a copy failure
        //    (transcript gone, perms, long filename) can't skip the breadcrumb.
        try {
          if (transcriptPath && existsSync(transcriptPath)) {
            mkdirSync(snapshotsDir(), { recursive: true });
            const stamp = new Date().toISOString().replace(/[:.]/g, "-");
            const dest = join(snapshotsDir(), `transcript-${sessionId}-${stamp}.jsonl`);
            copyFileSync(transcriptPath, dest);
            chmodSync(dest, 0o600); // snapshots can hold conversation content — owner-only
            pruneSnapshots(snapshotsDir());
          }
        } catch {
          // best-effort — fall through to the breadcrumb
        }
    
        // 2. Human/agent-readable breadcrumb — always attempted, even if (1) failed.
        writeFileSync(
          resumeFile(),
          `# Stitch session — resume breadcrumb\n\n` +
            `_Compaction (${trigger}) at ${new Date().toISOString()}._\n\n` +
            `${formatStatus(state)}\n`
        );
      }
    } catch {
      // Swallow everything — see the "ALWAYS exit 0" rule above.
    }
    
    process.exit(0);
    
  • hooks/session-start.mjsRunsGitHub
    Read the script
    #!/usr/bin/env node
    /**
     * session-start.mjs — SessionStart hook (matchers: compact, resume)
     *
     * Fires when a session begins or resumes. After a compaction the host re-runs
     * SessionStart with source "compact", which is our one chance to tell the
     * freshly-summarised model where its work actually lives. We emit the
     * re-orientation as `hookSpecificOutput.additionalContext` JSON — that's the
     * form both Claude Code and Codex inject as model-visible context (Codex treats
     * bare stdout as weaker "developer context", so JSON is the portable choice).
     *
     * Hard rule: this hook runs on EVERY session start for everyone who installs
     * stitch-kit. If there's no active Stitch session, it must print nothing and
     * get out of the way. And it must never throw — a crashing SessionStart hook is
     * a great way to ruin someone's day. So everything is wrapped and we always
     * exit 0.
     */
    
    import { readFileSync } from "node:fs";
    import { loadState, isRecent, formatStatus } from "../scripts/stitch-session.mjs";
    
    /** Sources where resurfacing state is useful. "startup"/"clear" are fresh starts — stay quiet there. */
    const RESURFACE_ON = new Set(["compact", "resume"]);
    
    try {
      // Hook input arrives as JSON on stdin. Parse source + cwd; if we can't read or
      // parse it, treat source as unknown and bail quietly rather than guessing.
      let source = "";
      try {
        const raw = readFileSync(0, "utf8");
        if (raw) {
          const input = JSON.parse(raw);
          if (input.cwd && !process.env.CLAUDE_PROJECT_DIR) process.env.CLAUDE_PROJECT_DIR = input.cwd;
          source = (input.source || "").toString();
        }
      } catch {
        source = "";
      }
    
      if (RESURFACE_ON.has(source)) {
        const state = loadState();
        if (state && isRecent(state)) {
          process.stdout.write(
            JSON.stringify({
              hookSpecificOutput: {
                hookEventName: "SessionStart",
                additionalContext: formatStatus(state),
              },
            }) + "\n"
          );
        }
      }
    } catch {
      // Never let a hook failure surface to the user or block the session.
    }
    
    process.exit(0);
    

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 withstitch-kit

Your coding agent writes decent code and designs terrible UI. stitch-kit fixes the second half — it wires agents into Google Stitch (text prompts → genuinely beautiful screens) and teaches them to drive it properly.

Get the whole plugin
Stats
45
Stars
5
Forks
Maintained
Maintenance
JavaScript
Language
Apache-2.0
License
2mo ago
Last commit
7mo ago
Created

Repo: gabelul/stitch-kit