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.
> /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
- Matches
autonode "${CLAUDE_PLUGIN_ROOT}/hooks/pre-compact.mjs" - Matches
manualnode "${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.
- Matches
compactnode "${CLAUDE_PLUGIN_ROOT}/hooks/session-start.mjs" - Matches
resumenode "${CLAUDE_PLUGIN_ROOT}/hooks/session-start.mjs"
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.
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.
Repo: gabelul/stitch-kit

