Commit 93e4a903a7b6

Vincent Demeester <vincent@sbr.pm>
2026-02-11 06:15:03
feat: migrate claude hooks to typescript, unify AI storage
Replaced Go-based claude-hooks with TypeScript hooks in dots/config/claude/hooks/, all writing to ~/.local/share/ai/ (shared with pi, opencode). Added path validation hook that blocks writes to old locations and enforces YYYY-MM-DD-slug.md naming. Removed claude-hooks Nix package from kyushu and aomi. Updated all skills, plugins, and configs to reference unified storage location.
1 parent f046048
dots/config/ai/path-policies.json
@@ -2,14 +2,14 @@
   "policies": [
     {
       "name": "ai-sessions-location",
-      "description": "Session files should go to unified AI storage",
-      "filenamePattern": ".*session.*\\.md$",
+      "description": "Session files must go to unified AI storage, not old Claude paths",
+      "pathPattern": ".*/(claude|.claude)/history/sessions/.*\\.md$",
       "blockedPaths": [
         "~/.config/claude/history/sessions/",
         "~/.claude/history/sessions/"
       ],
       "suggestedPath": "~/.local/share/ai/sessions/YYYY-MM/",
-      "action": "warn",
+      "action": "block",
       "enabled": true
     },
     {
@@ -24,14 +24,15 @@
     },
     {
       "name": "ai-plans-location",
-      "description": "Plan files should go to unified AI storage",
-      "filenamePattern": ".*plan.*\\.md$",
+      "description": "Plan files must go to unified AI storage",
+      "pathPattern": ".*/(claude|.claude)/(history/)?plans/.*\\.md$",
       "blockedPaths": [
         "~/.config/claude/plans/",
-        "~/.claude/plans/"
+        "~/.claude/plans/",
+        "~/.config/claude/history/plans/"
       ],
       "suggestedPath": "~/.local/share/ai/plans/",
-      "action": "warn",
+      "action": "block",
       "enabled": true
     },
     {
@@ -46,13 +47,13 @@
     },
     {
       "name": "ai-learnings-location",
-      "description": "Learning files should go to unified AI storage",
-      "filenamePattern": ".*learning.*\\.md$",
+      "description": "Learning files must go to unified AI storage",
+      "pathPattern": ".*/(claude|.claude)/history/learnings/.*\\.md$",
       "blockedPaths": [
         "~/.config/claude/history/learnings/"
       ],
       "suggestedPath": "~/.local/share/ai/learnings/YYYY-MM/",
-      "action": "warn",
+      "action": "block",
       "enabled": true
     },
     {
@@ -67,13 +68,13 @@
     },
     {
       "name": "ai-research-location",
-      "description": "Research files should go to unified AI storage",
-      "filenamePattern": ".*research.*\\.md$",
+      "description": "Research files must go to unified AI storage",
+      "pathPattern": ".*/(claude|.claude)/history/research/.*\\.md$",
       "blockedPaths": [
         "~/.config/claude/history/research/"
       ],
       "suggestedPath": "~/.local/share/ai/research/YYYY-MM/",
-      "action": "warn",
+      "action": "block",
       "enabled": true
     },
     {
dots/config/claude/hooks/.keep
dots/config/claude/hooks/capture-tool-output.ts
@@ -0,0 +1,61 @@
+#!/usr/bin/env bun
+/**
+ * PostToolUse hook — capture interesting tool outputs to JSONL.
+ *
+ * Tool-output logs are Claude-specific (not shared across tools)
+ * and stay under ~/.config/claude/history/tool-outputs/.
+ */
+
+import { readStdinJSON, TOOL_OUTPUTS_DIR, dateStr, yearMonth, ensureDir, now } from "./lib.ts";
+import { appendFileSync } from "node:fs";
+import { join } from "node:path";
+
+interface ToolUseData {
+  tool_name: string;
+  tool_input: Record<string, unknown>;
+  tool_response: Record<string, unknown>;
+  conversation_id?: string;
+  timestamp?: string;
+}
+
+const INTERESTING_TOOLS = new Set([
+  "Bash",
+  "Edit",
+  "Write",
+  "Read",
+  "Task",
+  "NotebookEdit",
+  "Skill",
+  "SlashCommand",
+]);
+
+async function main() {
+  const data = await readStdinJSON<ToolUseData>();
+  if (!data || !INTERESTING_TOOLS.has(data.tool_name)) {
+    process.exit(0);
+  }
+
+  const d = now();
+  const dir = join(TOOL_OUTPUTS_DIR, yearMonth(d));
+  ensureDir(dir);
+
+  const entry = {
+    timestamp: data.timestamp || d.toISOString(),
+    tool: data.tool_name,
+    input: data.tool_input,
+    output: data.tool_response,
+    session: data.conversation_id || "",
+  };
+
+  const file = join(dir, `${dateStr(d)}_tool-outputs.jsonl`);
+
+  try {
+    appendFileSync(file, JSON.stringify(entry) + "\n");
+  } catch (err) {
+    process.stderr.write(`[capture-tool-output] Error: ${err}\n`);
+  }
+
+  process.exit(0);
+}
+
+main();
dots/config/claude/hooks/initialize-session.ts
@@ -0,0 +1,52 @@
+#!/usr/bin/env bun
+/**
+ * SessionStart hook — initialise a Claude Code session.
+ *
+ * • Skips subagent sessions
+ * • Debounces duplicate triggers (2 s)
+ * • Sets terminal tab title
+ * • Logs session start to unified AI storage
+ */
+
+import {
+  isSubagent,
+  shouldDebounce,
+  setTerminalTitle,
+  sessionLogPath,
+  appendLog,
+  localTimestamp,
+  host,
+  now,
+} from "./lib.ts";
+
+function main() {
+  if (isSubagent()) {
+    process.stderr.write("🤖 Subagent session — skipping initialisation\n");
+    process.exit(0);
+  }
+
+  if (shouldDebounce("session-start")) {
+    process.stderr.write("⏱ Debouncing duplicate SessionStart\n");
+    process.exit(0);
+  }
+
+  const title = "Claude Ready";
+  setTerminalTitle(title);
+  process.stderr.write(`🚀 Session initialised: "${title}" on ${host()}\n`);
+
+  // Ring terminal bell
+  process.stderr.write("\x07");
+
+  // Append to unified session log (same format as pi ai-storage)
+  try {
+    const d = now();
+    const entry = `${localTimestamp(d)} - Session started (claude) on ${host()}\n`;
+    appendLog(sessionLogPath(d), entry);
+  } catch (err) {
+    process.stderr.write(`[initialize-session] Warning: ${err}\n`);
+  }
+
+  process.exit(0);
+}
+
+main();
dots/config/claude/hooks/lib.ts
@@ -0,0 +1,150 @@
+/**
+ * Shared utilities for Claude Code hooks.
+ * All hooks write to unified AI storage: ~/.local/share/ai/
+ */
+
+import { readFileSync, writeFileSync, mkdirSync, existsSync, appendFileSync } from "node:fs";
+import { join, basename } from "node:path";
+import { homedir, hostname } from "node:os";
+
+// ── Paths ──────────────────────────────────────────────────────────
+
+export const HOME = homedir();
+export const AI_DATA_DIR = join(HOME, ".local", "share", "ai");
+export const SESSIONS_DIR = join(AI_DATA_DIR, "sessions");
+export const RESEARCH_DIR = join(AI_DATA_DIR, "research");
+export const PLANS_DIR = join(AI_DATA_DIR, "plans");
+export const LEARNINGS_DIR = join(AI_DATA_DIR, "learnings");
+
+// Tool-output capture stays under claude config (not shared across tools)
+export const CLAUDE_DIR = process.env.CLAUDE_DIR || join(HOME, ".config", "claude");
+export const TOOL_OUTPUTS_DIR = join(CLAUDE_DIR, "history", "tool-outputs");
+
+// ── Date helpers (local time, matching pi ai-storage format) ───────
+
+export function now() {
+  return new Date();
+}
+
+/** Pad to 2 digits */
+function p2(n: number): string {
+  return String(n).padStart(2, "0");
+}
+
+/** Local timezone offset string, e.g. "+01:00" or "-05:00" */
+function tzOffset(d: Date): string {
+  const off = -d.getTimezoneOffset();
+  const sign = off >= 0 ? "+" : "-";
+  const h = p2(Math.floor(Math.abs(off) / 60));
+  const m = p2(Math.abs(off) % 60);
+  return `${sign}${h}:${m}`;
+}
+
+export function yearMonth(d: Date = now()): string {
+  return `${d.getFullYear()}-${p2(d.getMonth() + 1)}`;
+}
+
+export function dateStr(d: Date = now()): string {
+  return `${d.getFullYear()}-${p2(d.getMonth() + 1)}-${p2(d.getDate())}`;
+}
+
+/** ISO-ish local timestamp: YYYY-MM-DDTHH:MM:SS+TZ:TZ */
+export function localTimestamp(d: Date = now()): string {
+  return `${dateStr(d)}T${p2(d.getHours())}:${p2(d.getMinutes())}:${p2(d.getSeconds())}${tzOffset(d)}`;
+}
+
+/** HH:MM */
+export function timeStr(d: Date = now()): string {
+  return `${p2(d.getHours())}:${p2(d.getMinutes())}`;
+}
+
+// ── Subagent detection ─────────────────────────────────────────────
+
+export function isSubagent(): boolean {
+  const projectDir = process.env.CLAUDE_PROJECT_DIR || "";
+  if (projectDir.includes("/.claude/agents/")) return true;
+  if (process.env.CLAUDE_AGENT_TYPE) return true;
+  return false;
+}
+
+// ── Debounce via lockfile ──────────────────────────────────────────
+
+export function shouldDebounce(name: string, ms: number = 2000): boolean {
+  const lockfile = join("/tmp", `claude-${name}.lock`);
+  const nowMs = Date.now();
+
+  if (existsSync(lockfile)) {
+    try {
+      const prev = parseInt(readFileSync(lockfile, "utf-8").trim(), 10);
+      if (nowMs - prev < ms) return true;
+    } catch {}
+  }
+
+  try {
+    writeFileSync(lockfile, String(nowMs));
+  } catch {}
+  return false;
+}
+
+// ── Terminal title ─────────────────────────────────────────────────
+
+export function setTerminalTitle(title: string): void {
+  process.stderr.write(`\x1b]0;${title}\x07`);
+  process.stderr.write(`\x1b]2;${title}\x07`);
+  process.stderr.write(`\x1b]30;${title}\x07`);
+}
+
+// ── File helpers ───────────────────────────────────────────────────
+
+export function ensureDir(dir: string): void {
+  mkdirSync(dir, { recursive: true });
+}
+
+export function appendLog(file: string, line: string): void {
+  ensureDir(join(file, ".."));
+  appendFileSync(file, line);
+}
+
+export function sessionLogPath(d: Date = now()): string {
+  return join(SESSIONS_DIR, yearMonth(d), `${dateStr(d)}_session-log.txt`);
+}
+
+// ── Read stdin (for hooks that receive JSON) ───────────────────────
+
+export async function readStdin(): Promise<string> {
+  const chunks: Buffer[] = [];
+  for await (const chunk of process.stdin) {
+    chunks.push(chunk);
+  }
+  return Buffer.concat(chunks).toString("utf-8");
+}
+
+export async function readStdinJSON<T = any>(): Promise<T | null> {
+  try {
+    const raw = await readStdin();
+    if (!raw.trim()) return null;
+    return JSON.parse(raw) as T;
+  } catch {
+    return null;
+  }
+}
+
+// ── Hostname ───────────────────────────────────────────────────────
+
+export function host(): string {
+  return hostname();
+}
+
+// ── Slug helpers ───────────────────────────────────────────────────
+
+/**
+ * Normalise a description into a filename-safe slug.
+ * Target format: YYYY-MM-DD-description-with-dashes.md
+ */
+export function slugify(s: string): string {
+  return s
+    .toLowerCase()
+    .replace(/[^a-z0-9]+/g, "-")
+    .replace(/^-+|-+$/g, "")
+    .substring(0, 60);
+}
dots/config/claude/hooks/package.json
@@ -0,0 +1,6 @@
+{
+  "name": "claude-hooks",
+  "version": "1.0.0",
+  "type": "module",
+  "description": "TypeScript hooks for Claude Code — unified AI storage"
+}
dots/config/claude/hooks/save-session.ts
@@ -0,0 +1,87 @@
+#!/usr/bin/env bun
+/**
+ * SessionEnd hook — prompt Claude to save a session summary
+ * to the unified AI storage (~/.local/share/ai/sessions/).
+ */
+
+import { isSubagent, dateStr, yearMonth, now, SESSIONS_DIR } from "./lib.ts";
+
+function main() {
+  if (isSubagent()) {
+    process.exit(0);
+  }
+
+  // Ring terminal bell
+  process.stderr.write("\x07");
+
+  const d = now();
+  const ym = yearMonth(d);
+  const ds = dateStr(d);
+  const dir = `~/.local/share/ai/sessions/${ym}`;
+
+  process.stdout.write(`
+---
+
+# Automatic Session Summary
+
+**IMPORTANT**: Please create a session summary and save it to the unified AI storage.
+
+## Instructions
+
+Create a brief summary (2-4 paragraphs) of this session documenting:
+- Primary tasks accomplished
+- Key decisions or solutions
+- Files/systems modified
+- Any remaining work or next steps
+
+Save the summary using the Write tool to:
+\`${dir}/${ds}-<brief-slug>.md\`
+
+**Filename format**: \`YYYY-MM-DD-description-with-dashes.md\`
+- Use only lowercase letters, digits, and dashes
+- Keep description to 3-6 words
+- No underscores, no timestamps beyond the date, no _SESSION_ prefix
+
+**Good examples:**
+- \`${ds}-nix-systems-refactoring.md\`
+- \`${ds}-tekton-pr-review.md\`
+- \`${ds}-pi-extensions-setup.md\`
+
+**Bad examples:**
+- \`${ds}_SESSION_some-thing.md\` (underscore, SESSION prefix)
+- \`${ds}-155616_SESSION_description.md\` (timestamp noise)
+- \`some-thing.md\` (missing date)
+
+Use the **Session Entry** template:
+
+\`\`\`markdown
+# Session: <Description>
+
+**Date:** ${ds}
+**Tool:** claude
+
+## Summary
+Brief description of what was accomplished.
+
+## What Was Accomplished
+- Task 1
+- Task 2
+
+## Files Changed
+- \\\`path/to/file\\\` - Description of change
+
+## Outcome
+Result of the session.
+
+## Next Steps
+- [ ] Follow-up task
+
+### Tags
+#tag1 #tag2
+\`\`\`
+`);
+
+  process.exit(0);
+}
+
+main();
dots/config/claude/hooks/update-terminal-title.ts
@@ -0,0 +1,84 @@
+#!/usr/bin/env bun
+/**
+ * PostToolUse hook — update terminal tab title with context.
+ *
+ * Shows project name, active skill, task progress.
+ */
+
+import { readStdinJSON, setTerminalTitle } from "./lib.ts";
+import { basename } from "node:path";
+
+interface ToolResult {
+  tool_name: string;
+  tool_input: Record<string, unknown>;
+}
+
+interface TodoItem {
+  content: string;
+  status: string;
+  activeForm?: string;
+}
+
+const SKILL_ICONS: Record<string, string> = {
+  Journal: "📓",
+  Notes: "📝",
+  TODOs: "✅",
+  Org: "📋",
+  Git: "🔀",
+  GitHub: "🐙",
+  Email: "📧",
+  Python: "🐍",
+  golang: "🐹",
+  Rust: "🦀",
+  Nix: "❄️",
+  Kubernetes: "☸️",
+  Tekton: "🔧",
+};
+
+function getTodoProgress(todos: TodoItem[]): string | null {
+  if (!todos?.length) return null;
+  let completed = 0;
+  let active = "";
+  for (const t of todos) {
+    if (t.status === "completed") completed++;
+    if (t.status === "in_progress" && t.activeForm) active = t.activeForm;
+  }
+  return active ? `[${completed + 1}/${todos.length}] ${active}` : null;
+}
+
+function buildTitle(result: ToolResult | null): string {
+  const prefix = "Claude";
+  const parts: string[] = [];
+
+  if (result?.tool_name === "TodoWrite") {
+    const todos = (result.tool_input as any).todos as TodoItem[] | undefined;
+    if (todos) {
+      const progress = getTodoProgress(todos);
+      if (progress) parts.push(progress);
+    }
+  }
+
+  if (result?.tool_name === "Skill") {
+    const skill = (result.tool_input as any).skill as string | undefined;
+    if (skill) {
+      const icon = SKILL_ICONS[skill] || "🎯";
+      parts.push(`${icon} ${skill}`);
+    }
+  }
+
+  const projectDir = process.env.CLAUDE_PROJECT_DIR;
+  if (projectDir) parts.push(basename(projectDir));
+
+  try {
+    parts.push(basename(process.cwd()));
+  } catch {}
+
+  return parts.length ? `${prefix} ${parts.join(" • ")}` : `${prefix} Ready`;
+}
+
+async function main() {
+  const result = await readStdinJSON<ToolResult>();
+  setTerminalTitle(buildTitle(result));
+}
+
+main();
dots/config/claude/hooks/validate-git-push.ts
@@ -0,0 +1,104 @@
+#!/usr/bin/env bun
+/**
+ * PreToolUse hook (Bash) — validate git commands for safety.
+ *
+ * Matches pi's validate-git-push.ts extension behaviour:
+ *   • git push without explicit refspec  → block
+ *   • git add -A / --all / .             → block
+ *   • push to protected branches         → warn (stderr)
+ *
+ * Handles command chaining: &&, ||, ;, |, $()
+ */
+
+import { readStdinJSON } from "./lib.ts";
+
+interface PreToolInput {
+  tool_name: string;
+  tool_input: { command?: string };
+}
+
+// Match git commands even after chaining operators
+const CMD_PREFIX = String.raw`(^|&&|\|\||;|\||\$\()\s*`;
+
+const DANGEROUS_ADD = [
+  new RegExp(CMD_PREFIX + String.raw`git\s+add\s+-A(\s|$)`),
+  new RegExp(CMD_PREFIX + String.raw`git\s+add\s+--all(\s|$)`),
+  new RegExp(CMD_PREFIX + String.raw`git\s+add\s+\.(\s|$)`),
+];
+
+const GIT_PUSH = new RegExp(CMD_PREFIX + String.raw`git\s+push(\s|$)`);
+const HAS_REFSPEC = /git\s+push\s+.*\s+\S+:\S+/;
+
+function isDangerousAdd(cmd: string): boolean {
+  return DANGEROUS_ADD.some((re) => re.test(cmd));
+}
+
+function isGitPush(cmd: string): boolean {
+  return GIT_PUSH.test(cmd);
+}
+
+function pushesToProtected(cmd: string): boolean {
+  return [":main", ":master"].some((b) => cmd.includes(b));
+}
+
+async function main() {
+  const data = await readStdinJSON<PreToolInput>();
+  if (!data) process.exit(0);
+
+  const cmd = data.tool_input?.command?.trim() || "";
+  if (!cmd) process.exit(0);
+
+  // ── git add safety ────────────────────────────────────────────
+  if (isDangerousAdd(cmd)) {
+    process.stdout.write(
+      JSON.stringify({
+        decision: "block",
+        reason: [
+          "BLOCKED: Dangerous git add command detected!",
+          "",
+          "Commands like 'git add -A', 'git add --all', and 'git add .' can add unintended files.",
+          "",
+          "Use explicit file paths instead:",
+          "  git add path/to/specific/file.txt",
+          "  git add path/to/directory/",
+          "",
+          `Blocked command: ${cmd}`,
+        ].join("\n"),
+      })
+    );
+    process.exit(0);
+  }
+
+  // ── git push safety ───────────────────────────────────────────
+  if (isGitPush(cmd)) {
+    if (!HAS_REFSPEC.test(cmd)) {
+      process.stdout.write(
+        JSON.stringify({
+          decision: "block",
+          reason: [
+            "BLOCKED: git push without explicit refspec!",
+            "",
+            "Implicit branch tracking can push to wrong branches.",
+            "",
+            "Use explicit refspec instead:",
+            "  git push origin <branch>:<branch>",
+            "  git push origin HEAD:<branch>",
+            "",
+            `Blocked command: ${cmd}`,
+          ].join("\n"),
+        })
+      );
+      process.exit(0);
+    }
+
+    if (pushesToProtected(cmd)) {
+      process.stderr.write(
+        "⚠️  Warning: pushing to protected branch (main/master)\n"
+      );
+    }
+  }
+
+  process.exit(0);
+}
+
+main();
dots/config/claude/hooks/validate-write-path.ts
@@ -0,0 +1,272 @@
+#!/usr/bin/env bun
+/**
+ * PreToolUse hook (Write) — validate file paths and filenames.
+ *
+ * Mirrors pi's path-validator extension:
+ *   • Reads policies from ~/.config/ai/path-policies.json
+ *   • Blocks writes to wrong locations (old claude history paths)
+ *   • Enforces filename conventions (YYYY-MM-DD-slug.md)
+ *   • Falls back to built-in defaults if no config found
+ */
+
+import { readStdinJSON, HOME } from "./lib.ts";
+import { readFileSync, existsSync } from "node:fs";
+import { join, basename } from "node:path";
+
+// ── Types ──────────────────────────────────────────────────────────
+
+interface PreToolInput {
+  tool_name: string;
+  tool_input: {
+    file_path?: string;
+    filePath?: string;
+    path?: string;
+    command?: string;
+  };
+}
+
+interface PathPolicy {
+  name: string;
+  description?: string;
+  filenamePattern?: string;
+  pathPattern?: string;
+  allowedPaths?: string[];
+  blockedPaths?: string[];
+  requiredFilenameFormat?: string;
+  formatDescription?: string;
+  formatExample?: string;
+  suggestedPath?: string;
+  action?: "warn" | "block" | "suggest";
+  enabled?: boolean;
+}
+
+interface PolicyConfig {
+  policies: PathPolicy[];
+}
+
+// ── Helpers ────────────────────────────────────────────────────────
+
+function expandHome(p: string): string {
+  return p.startsWith("~/") ? join(HOME, p.slice(2)) : p;
+}
+
+function pathMatches(path: string, pattern: string): boolean {
+  const expanded = expandHome(pattern)
+    .replace(/[.+^${}()|[\]\\]/g, "\\$&")
+    .replace(/\*/g, ".*")
+    .replace(/\?/g, ".");
+  return new RegExp(`^${expanded}`).test(path);
+}
+
+// ── Default policies (same as pi path-validator) ───────────────────
+
+const DEFAULT_POLICIES: PathPolicy[] = [
+  {
+    name: "ai-sessions-location",
+    description: "Session files must go to unified AI storage, not old Claude paths",
+    filenamePattern: ".*session.*\\.md$",
+    blockedPaths: [
+      "~/.config/claude/history/sessions/",
+      "~/.claude/history/sessions/",
+    ],
+    suggestedPath: "~/.local/share/ai/sessions/YYYY-MM/",
+    action: "block",
+    enabled: true,
+  },
+  {
+    name: "ai-sessions-format",
+    description: "Session files must follow YYYY-MM-DD-description.md format",
+    pathPattern: ".*/ai/sessions/.*\\.md$",
+    requiredFilenameFormat: "^\\d{4}-\\d{2}-\\d{2}-[a-z0-9][a-z0-9-]*\\.md$",
+    formatDescription: "YYYY-MM-DD-description.md (lowercase, hyphens only, no underscores)",
+    formatExample: "2026-02-05-unified-ai-storage.md",
+    action: "block",
+    enabled: true,
+  },
+  {
+    name: "ai-plans-location",
+    description: "Plan files must go to unified AI storage",
+    filenamePattern: ".*plan.*\\.md$",
+    blockedPaths: [
+      "~/.config/claude/plans/",
+      "~/.claude/plans/",
+      "~/.config/claude/history/plans/",
+    ],
+    suggestedPath: "~/.local/share/ai/plans/",
+    action: "block",
+    enabled: true,
+  },
+  {
+    name: "ai-plans-format",
+    description: "Plan files must follow kebab-case.md format (no dates)",
+    pathPattern: ".*/ai/plans/.*\\.md$",
+    requiredFilenameFormat: "^[a-z0-9][a-z0-9-]*\\.md$",
+    formatDescription: "kebab-case.md (lowercase, hyphens, no dates)",
+    formatExample: "homelab-migration-plan.md",
+    action: "block",
+    enabled: true,
+  },
+  {
+    name: "ai-learnings-location",
+    description: "Learning files must go to unified AI storage",
+    filenamePattern: ".*learning.*\\.md$",
+    blockedPaths: ["~/.config/claude/history/learnings/"],
+    suggestedPath: "~/.local/share/ai/learnings/YYYY-MM/",
+    action: "block",
+    enabled: true,
+  },
+  {
+    name: "ai-learnings-format",
+    description: "Learning files must follow YYYY-MM-DD-description.md format",
+    pathPattern: ".*/ai/learnings/.*\\.md$",
+    requiredFilenameFormat: "^\\d{4}-\\d{2}-\\d{2}-[a-z0-9][a-z0-9-]*\\.md$",
+    formatDescription: "YYYY-MM-DD-description.md (lowercase, hyphens only)",
+    formatExample: "2026-02-05-nix-flake-patterns.md",
+    action: "block",
+    enabled: true,
+  },
+  {
+    name: "ai-research-location",
+    description: "Research files must go to unified AI storage",
+    filenamePattern: ".*research.*\\.md$",
+    blockedPaths: ["~/.config/claude/history/research/"],
+    suggestedPath: "~/.local/share/ai/research/YYYY-MM/",
+    action: "block",
+    enabled: true,
+  },
+  {
+    name: "ai-research-format",
+    description: "Research files must follow YYYY-MM-DD-description.md format",
+    pathPattern: ".*/ai/research/.*\\.md$",
+    requiredFilenameFormat: "^\\d{4}-\\d{2}-\\d{2}-[a-z0-9][a-z0-9-]*\\.md$",
+    formatDescription: "YYYY-MM-DD-description.md (lowercase, hyphens only)",
+    formatExample: "2026-02-05-chrono-node-evaluation.md",
+    action: "block",
+    enabled: true,
+  },
+  {
+    name: "no-secrets-in-repos",
+    description: "Prevent writing secrets to git repositories",
+    filenamePattern: "\\.(env|pem|key|secret|credentials)$",
+    blockedPaths: ["~/src/*", "~/projects/*"],
+    action: "block",
+    enabled: true,
+  },
+];
+
+// ── Load policies ──────────────────────────────────────────────────
+
+function loadPolicies(): PathPolicy[] {
+  const configPath = join(HOME, ".config", "ai", "path-policies.json");
+  try {
+    if (existsSync(configPath)) {
+      const config: PolicyConfig = JSON.parse(
+        readFileSync(configPath, "utf-8")
+      );
+      if (config.policies && Array.isArray(config.policies)) {
+        const names = new Set(config.policies.map((p) => p.name));
+        return [
+          ...config.policies,
+          ...DEFAULT_POLICIES.filter((p) => !names.has(p.name)),
+        ];
+      }
+    }
+  } catch {}
+  return DEFAULT_POLICIES;
+}
+
+// ── Validate ───────────────────────────────────────────────────────
+
+function validate(
+  filePath: string,
+  policies: PathPolicy[]
+): { blocks: string[]; warns: string[] } {
+  const expanded = expandHome(filePath);
+  const fname = basename(expanded);
+  const blocks: string[] = [];
+  const warns: string[] = [];
+
+  for (const pol of policies) {
+    if (pol.enabled === false) continue;
+
+    // Check if this policy applies (filename or path pattern)
+    let matches = false;
+    if (pol.filenamePattern && new RegExp(pol.filenamePattern, "i").test(fname))
+      matches = true;
+    if (pol.pathPattern && new RegExp(pol.pathPattern, "i").test(expanded))
+      matches = true;
+    if (!matches) continue;
+
+    const action = pol.action || "warn";
+    const bucket = action === "block" ? blocks : warns;
+
+    // Check blocked paths
+    if (pol.blockedPaths) {
+      for (const bp of pol.blockedPaths) {
+        if (pathMatches(expanded, bp)) {
+          let msg = `[${pol.name}] ${pol.description || "Blocked path"}`;
+          if (pol.suggestedPath) msg += `\n   → Use: ${pol.suggestedPath}`;
+          bucket.push(msg);
+          break;
+        }
+      }
+    }
+
+    // Check allowed paths
+    if (pol.allowedPaths?.length) {
+      const ok = pol.allowedPaths.some((ap) => pathMatches(expanded, ap));
+      if (!ok) {
+        bucket.push(
+          `[${pol.name}] Path not in allowed locations: ${pol.allowedPaths.join(", ")}`
+        );
+      }
+    }
+
+    // Check required filename format
+    if (pol.requiredFilenameFormat) {
+      if (!new RegExp(pol.requiredFilenameFormat).test(fname)) {
+        const desc = pol.formatDescription || pol.requiredFilenameFormat;
+        const ex = pol.formatExample ? `\n   Example: ${pol.formatExample}` : "";
+        bucket.push(`[${pol.name}] Filename doesn't match format: ${desc}${ex}`);
+      }
+    }
+  }
+
+  return { blocks, warns };
+}
+
+// ── Main ───────────────────────────────────────────────────────────
+
+async function main() {
+  const data = await readStdinJSON<PreToolInput>();
+  if (!data) process.exit(0);
+
+  const filePath =
+    data.tool_input?.file_path ||
+    data.tool_input?.filePath ||
+    data.tool_input?.path;
+
+  if (!filePath || typeof filePath !== "string") process.exit(0);
+
+  const policies = loadPolicies();
+  const { blocks, warns } = validate(filePath, policies);
+
+  // Stderr warnings (non-blocking)
+  for (const w of warns) {
+    process.stderr.write(`⚠️  ${w}\n`);
+  }
+
+  // Block if violations
+  if (blocks.length > 0) {
+    process.stdout.write(
+      JSON.stringify({
+        decision: "block",
+        reason: ["BLOCKED: Path policy violation", "", ...blocks].join("\n"),
+      })
+    );
+  }
+
+  process.exit(0);
+}
+
+main();
dots/config/claude/plugins/session-manager/commands/save-session.md
@@ -1,16 +1,16 @@
 ---
-description: Save a summary of the current Claude Code session to history
+description: Save a summary of the current Claude Code session to unified AI storage
 ---
 
 # Save Session
 
-Create a comprehensive session summary following the history-system format and save it to `~/.config/claude/history/sessions/`.
+Create a comprehensive session summary and save it to the unified AI storage at `~/.local/share/ai/sessions/`.
 
 ## Important Guidelines
 
 - **DO NOT use echo or command-line tools to communicate** - output all text directly in your response
-- **Create a properly formatted session entry** following the history-system skill template
-- **Save to the correct location** with proper timestamp and filename format
+- **Create a properly formatted session entry** following the template below
+- **Save to the correct location** with proper filename format
 - **Be concise but comprehensive** - capture what matters
 
 ---
@@ -23,158 +23,76 @@ Review the conversation and identify:
 1. **Main accomplishments** - What was actually done?
 2. **Key decisions** - What choices were made and why?
 3. **Next steps** - What follow-up work is needed?
-4. **Related topics** - What areas of the codebase or system were touched?
 
 ### Step 2: Generate Session Entry
 
-Create a session entry file with:
+**Filename format**: `YYYY-MM-DD-description-with-dashes.md`
 
-**Filename format**: `YYYY-MM-DD-HHMMSS_SESSION_description.md`
+Rules:
+- Use only lowercase letters, digits, and dashes in the description
+- Keep description to 3-6 words
+- **NO underscores** in filenames
+- **NO timestamps** beyond the date
+- **NO `_SESSION_` prefix**
 
-**Location**: `~/.config/claude/history/sessions/YYYY-MM/`
+**Location**: `~/.local/share/ai/sessions/YYYY-MM/`
 
 **Template**:
 ```markdown
 # Session: <Brief Description>
 
-**Date**: YYYY-MM-DD HH:MM
-**Duration**: <estimated duration if known>
-**Context**: <What prompted this work session>
+**Date:** YYYY-MM-DD
+**Tool:** claude
+
+## Summary
+Brief description of what was accomplished.
 
 ## What Was Accomplished
-- <Specific task 1>
-- <Specific task 2>
-- <Specific task 3>
+- Task 1
+- Task 2
 
-## Decisions Made
-- **<Decision>**: <Rationale>
-- **<Decision>**: <Rationale>
+## Files Changed
+- `path/to/file` - Description of change
 
-## Technical Details
-<Any important technical information worth preserving>
+## Outcome
+Result of the session.
 
 ## Next Steps
-- [ ] <Follow-up task 1>
-- [ ] <Follow-up task 2>
+- [ ] Follow-up task 1
+- [ ] Follow-up task 2
 
-## Related Notes
-<If applicable, link to related denote notes>
-
-## Tags
-#session #<relevant-topic-tags>
+### Tags
+#claude #relevant-topic-tags
 ```
 
 ### Step 3: Save the File
 
 Use the Write tool to save the session entry to:
 ```
-~/.config/claude/history/sessions/YYYY-MM/YYYY-MM-DD-HHMMSS_SESSION_description.md
+~/.local/share/ai/sessions/YYYY-MM/YYYY-MM-DD-description.md
 ```
 
-Replace:
-- `YYYY-MM` with current year-month
-- `YYYY-MM-DD-HHMMSS` with current timestamp
-- `description` with a short hyphenated description (e.g., `nixos-syncthing-config`)
+**Good examples:**
+- `~/.local/share/ai/sessions/2026-02/2026-02-10-tekton-pr-review.md`
+- `~/.local/share/ai/sessions/2026-02/2026-02-10-nix-systems-refactoring.md`
+
+**Bad examples:**
+- `~/.local/share/ai/sessions/2026-02/2026-02-10_SESSION_something.md` ❌
+- `~/.config/claude/history/sessions/2026-02/...` ❌ (old location!)
 
 ### Step 4: Confirm to User
 
 After saving, inform the user:
 ```
-✅ Session saved to: ~/.config/claude/history/sessions/YYYY-MM/YYYY-MM-DD-HHMMSS_SESSION_description.md
-
-Summary:
-- X tasks completed
-- Y decisions documented
-- Z next steps identified
+✅ Session saved to: ~/.local/share/ai/sessions/YYYY-MM/YYYY-MM-DD-description.md
 ```
 
 ---
 
-## Examples
-
-### Example 1: Configuration Change Session
-```markdown
-# Session: Configure Syncthing for Claude History
-
-**Date**: 2025-12-04 07:30
-**Duration**: 30 minutes
-**Context**: Need to sync Claude Code history across machines for backup
-
-## What Was Accomplished
-- Added claude-history folder to globals.nix syncthingFolders
-- Generated syncthing folder ID (j5zdn-6kq4t)
-- Configured claude-history sync for kyushu and aomi
-- Path: /home/vincent/.config/claude/history
-
-## Decisions Made
-- **Syncthing ID format**: Used random 5-char strings (xxxxx-xxxxx) following existing pattern
-- **Machines**: Started with kyushu and aomi only, can add more later
-
-## Technical Details
-- Syncthing folder ID: j5zdn-6kq4t
-- Path: /home/vincent/.config/claude/history
-- Synced machines: kyushu (SBLRZF4-...), aomi (CN5P3MV-...)
-
-## Next Steps
-- [ ] Rebuild NixOS on kyushu to activate sync
-- [ ] Rebuild NixOS on aomi to activate sync
-- [ ] Verify syncthing picks up the new folder
-- [ ] Consider adding more machines later
-
-## Tags
-#session #nixos #syncthing #claude-code #homelab
-```
-
-### Example 2: Development Session
-```markdown
-# Session: Implement Session Saving Hooks
-
-**Date**: 2025-12-04 08:00
-**Duration**: 45 minutes
-**Context**: Automate session saving in Claude Code
-
-## What Was Accomplished
-- Created claude-hooks-save-session Go command
-- Created /save-session slash command plugin
-- Updated plugin structure in dots/.claude/plugins/session-manager
-- Documented workflow in command file
-
-## Decisions Made
-- **SessionEnd hook approach**: Prompt user instead of auto-save (user agency)
-- **Plugin structure**: Created dedicated session-manager plugin
-- **Format**: Follow existing history-system markdown template
-
-## Technical Details
-- Hook location: tools/claude-hooks/cmd/save-session/
-- Plugin location: dots/.claude/plugins/session-manager/
-- Follows existing claude-hooks Go patterns
-
-## Next Steps
-- [ ] Update default.nix to build save-session command
-- [ ] Add SessionEnd hook to settings.json
-- [ ] Test hook and slash command
-- [ ] Update setup-hooks.sh
-
-## Tags
-#session #development #claude-code #golang #hooks
-```
-
----
-
-## Best Practices
-
-1. **Be specific** - Don't just say "worked on code", say what exactly was modified
-2. **Capture decisions** - Document WHY choices were made, not just what was done
-3. **Include context** - Future you needs to know what prompted this work
-4. **Link related work** - Reference denote notes, commits, or other sessions
-5. **Add tags** - Make sessions searchable by topic
-6. **Keep it real** - If nothing significant happened, it's okay to skip saving
-
 ## When to Use
 
 - After completing significant work
 - When making important architectural decisions
-- When learning something worth preserving
 - At the end of productive sessions
 - When asked by SessionEnd hook
 
@@ -183,4 +101,3 @@ Summary:
 - Very short sessions (<5 minutes)
 - Purely exploratory work with no findings
 - When no meaningful progress was made
-- When the work will be documented elsewhere
dots/config/claude/skills/CORE/history-system.md
@@ -1,14 +1,15 @@
 # History System Documentation
 
-**Location:** `~/.config/claude/history/`
+**Location:** `~/.local/share/ai/` (unified across all AI coding tools)
 **Format:** Markdown (`.md`) only
-**Purpose:** Automated documentation of work performed by Claude Code
+**Purpose:** Automated documentation of work performed by AI coding tools
+**Synced:** Via syncthing (actual data in `~/.local/share/ai-sync/`)
 
 ---
 
 ## Overview
 
-The History System captures significant work and learnings for future reference. All entries are markdown files, synced across machines via syncthing.
+The History System captures significant work and learnings for future reference. All entries are markdown files, stored in a unified location shared by all AI coding tools (Claude Code, pi, opencode, etc.) and synced across machines via syncthing.
 
 ## File Format
 
@@ -19,13 +20,30 @@ The History System captures significant work and learnings for future reference.
 - Easy to read in any editor
 - Searchable with grep/rg
 
+## Filename Convention
+
+**STRICT FORMAT:** `YYYY-MM-DD-description-with-dashes.md`
+
+Rules:
+- Date prefix is mandatory
+- Use only lowercase letters, digits, and dashes
+- **NO underscores** (except `_session-log.txt`)
+- **NO timestamps** beyond the date (no `_155616_SESSION_`)
+- **NO _SESSION_ prefix**
+- Keep description to 3-6 words, max 60 characters for the slug
+- Slug is generated: `title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").substring(0, 60)`
+
+Good: `2026-02-10-tekton-pr-review.md`
+Bad:  `2026-02-10_155616_SESSION_tekton-pr-review.md`
+
 ## Directory Structure
 
 ```
-~/.config/claude/history/
+~/.local/share/ai/
 ├── sessions/                 # Work session summaries
 │   └── YYYY-MM/
-│       └── YYYY-MM-DD-description.md
+│       ├── YYYY-MM-DD-description.md
+│       └── YYYY-MM-DD_session-log.txt
 ├── learnings/                # Problem-solving insights
 │   └── YYYY-MM/
@@ -35,19 +53,16 @@ The History System captures significant work and learnings for future reference.
 │   └── YYYY-MM/
 │       └── YYYY-MM-DD-topic.md
-├── decisions/                # Architecture decisions
-│   └── YYYY-MM/
-│       └── YYYY-MM-DD-description.md
+├── plans/                    # Project plans (NOT date-organized)
+│   └── plan-name.md
-├── execution/                # Command/feature execution logs
-│   └── YYYY-MM/
-│       └── YYYY-MM-DD-description.md
-│
-└── tool-outputs/             # Tool execution logs (auto-generated)
-    └── YYYY-MM/
-        └── YYYY-MM-DD_tool-outputs.jsonl
+└── .pending/                 # Auto-saved transcripts (temporary)
+    └── *.json
 ```
 
+Note: Tool-specific data (like tool-outputs JSONL) stays under tool config:
+- Claude: `~/.config/claude/history/tool-outputs/`
+
 **Quick Decision Guide:**
 - "What happened this session?" → `sessions/`
 - "What did we learn?" → `learnings/`
@@ -157,25 +172,22 @@ Relevant commands or code snippets.
 ### By Category
 ```bash
 # Find all sessions
-ls ~/.config/claude/history/sessions/*/*.md
+ls ~/.local/share/ai/sessions/*/*.md
 
 # Find all learnings
-ls ~/.config/claude/history/learnings/*/*.md
+ls ~/.local/share/ai/learnings/*/*.md
 
 # Find recent research
-ls -lt ~/.config/claude/history/research/
+ls -lt ~/.local/share/ai/research/
 
-# Find all decisions
-ls ~/.config/claude/history/decisions/*/*.md
-
-# Find all executions
-ls ~/.config/claude/history/execution/*/*.md
+# Find all plans
+ls ~/.local/share/ai/plans/*.md
 ```
 
 ### By Content
 ```bash
 # Search all history
-rg "keyword" ~/.config/claude/history/
+rg "keyword" ~/.local/share/ai/
 ```
 
 ## Quick Reference
@@ -183,15 +195,15 @@ rg "keyword" ~/.config/claude/history/
 ### Create Session Entry
 ```bash
 DATE=$(date +"%Y-%m")
-mkdir -p ~/.config/claude/history/sessions/$DATE
-vim ~/.config/claude/history/sessions/$DATE/$(date +"%Y-%m-%d")-description.md
+mkdir -p ~/.local/share/ai/sessions/$DATE
+vim ~/.local/share/ai/sessions/$DATE/$(date +"%Y-%m-%d")-description.md
 ```
 
 ### Create Learning Entry
 ```bash
 DATE=$(date +"%Y-%m")
-mkdir -p ~/.config/claude/history/learnings/$DATE
-vim ~/.config/claude/history/learnings/$DATE/$(date +"%Y-%m-%d")-description.md
+mkdir -p ~/.local/share/ai/learnings/$DATE
+vim ~/.local/share/ai/learnings/$DATE/$(date +"%Y-%m-%d")-description.md
 ```
 
 **Remember:** When in doubt, save to history! It's better to capture too much than to lose valuable insights.
dots/config/claude/skills/CORE/hook-system.md
@@ -2,20 +2,22 @@
 
 **Event-Driven Automation Infrastructure**
 
-**Location:** `/home/vincent/src/home/tools/claude-hooks/`
-**Configuration:** `/home/vincent/.config/claude/settings.json`
-**Status:** Active - Go-based implementation
+**Location:** `~/.config/claude/hooks/` (source: `dots/config/claude/hooks/`)
+**Configuration:** `~/.config/claude/settings.json`
+**Status:** Active - TypeScript/Bun implementation
 
 ---
 
 ## Overview
 
-The hook system is an event-driven automation infrastructure built on Claude Code's native hook support. Hooks are executable commands (Go binaries) that run automatically in response to specific events during Claude Code sessions.
+The hook system is an event-driven automation infrastructure built on Claude Code's native hook support. Hooks are TypeScript scripts (run via `bun`) that execute automatically in response to specific events during Claude Code sessions.
 
 **Core Capabilities:**
+- **Unified AI Storage** - All sessions, logs write to `~/.local/share/ai/` (shared with pi, opencode)
 - **Session Management** - Set terminal titles, log session starts
 - **Tool Output Capture** - Automatic logging of tool executions to JSONL files
 - **Session Prompts** - Prompt to save session summaries on exit
+- **Git Safety** - Block dangerous git operations (push without refspec, add -A)
 - **Subagent Detection** - Skip hooks for subagent sessions
 
 **Key Principle:** Hooks run asynchronously and fail gracefully. They enhance the user experience but never block Claude Code's core functionality.
@@ -25,24 +27,19 @@ The hook system is an event-driven automation infrastructure built on Claude Cod
 ## Architecture
 
 ```
-/home/vincent/src/home/tools/claude-hooks/
-├── cmd/
-│   ├── initialize-session/main.go     # SessionStart hook
-│   ├── capture-tool-output/main.go    # PostToolUse hook
-│   ├── save-session/main.go           # SessionEnd hook
-│   └── validate-docs/main.go          # Documentation validator (not used as hook)
-├── internal/
-│   └── paths/paths.go                 # Shared path utilities
-├── default.nix                        # Nix package definition
-├── go.mod                             # Go module definition
-├── go.sum                             # Go dependencies
-├── setup-hooks.sh                     # Configuration script
-└── README.md                          # Documentation
+dots/config/claude/hooks/          # Source (in homelab repo)
+  → ~/.config/claude/hooks/        # Symlinked at runtime
+├── lib.ts                         # Shared utilities (paths, dates, slugs)
+├── initialize-session.ts          # SessionStart hook
+├── save-session.ts                # SessionEnd hook
+├── capture-tool-output.ts         # PostToolUse hook
+├── update-terminal-title.ts       # PostToolUse hook
+├── validate-git-push.ts           # PreToolUse (Bash) hook
+└── package.json                   # Dependencies
 ```
 
-**Implementation:** Go binaries compiled to native code for zero runtime dependencies, faster execution, and cross-platform compatibility.
-
-**Migration:** Migrated from TypeScript/Bun implementation previously in `dots/.claude/hooks/`.
+**Implementation:** TypeScript scripts executed via `bun run`. Shared utilities in `lib.ts`.
+**Storage:** Sessions and logs go to `~/.local/share/ai/` (unified with pi ai-storage extension).
 
 ---
 
@@ -52,7 +49,7 @@ You currently have 3 hooks configured. Claude Code supports 8 hook event types t
 
 ### 1. **SessionStart**
 **When:** Claude Code session begins (new conversation)
-**Command:** `claude-hooks-initialize-session`
+**Command:** `bun run ~/.config/claude/hooks/initialize-session.ts`
 
 **Current Configuration:**
 ```json
@@ -62,7 +59,7 @@ You currently have 3 hooks configured. Claude Code supports 8 hook event types t
       "hooks": [
         {
           "type": "command",
-          "command": "claude-hooks-initialize-session"
+          "command": "bun run ~/.config/claude/hooks/initialize-session.ts"
         }
       ]
     }
@@ -73,7 +70,7 @@ You currently have 3 hooks configured. Claude Code supports 8 hook event types t
 **What It Does:**
 - **Detects subagent sessions**: Skips if `CLAUDE_AGENT_TYPE` is set or path contains `/.claude/agents/`
 - **Sets terminal tab title**: Sets tab title to "Claude Ready" using ANSI escape codes
-- **Logs session start**: Appends to `~/.config/claude/history/sessions/YYYY-MM/YYYY-MM-DD_session-log.txt`
+- **Logs session start**: Appends to `~/.local/share/ai/sessions/YYYY-MM/YYYY-MM-DD_session-log.txt`
 - **Implements debouncing**: 2-second window to prevent duplicate triggers
 
 **Implementation Details:**
@@ -119,7 +116,7 @@ Uses a lockfile in `/tmp/claude-session-start.lock` with timestamps to prevent d
 
 ### 2. **PostToolUse**
 **When:** After Claude executes any tool
-**Command:** `claude-hooks-capture-tool-output`
+**Command:** `bun run ~/.config/claude/hooks/capture-tool-output.ts`
 
 **Current Configuration:**
 ```json
@@ -129,7 +126,7 @@ Uses a lockfile in `/tmp/claude-session-start.lock` with timestamps to prevent d
       "hooks": [
         {
           "type": "command",
-          "command": "claude-hooks-capture-tool-output"
+          "command": "bun run ~/.config/claude/hooks/capture-tool-output.ts"
         }
       ]
     }
@@ -199,7 +196,7 @@ Claude Code sends JSON data on stdin:
 
 ### 3. **SessionEnd**
 **When:** Claude Code session terminates (conversation ends)
-**Command:** `claude-hooks-save-session`
+**Command:** `bun run ~/.config/claude/hooks/save-session.ts`
 
 **Current Configuration:**
 ```json
@@ -209,7 +206,7 @@ Claude Code sends JSON data on stdin:
       "hooks": [
         {
           "type": "command",
-          "command": "claude-hooks-save-session"
+          "command": "bun run ~/.config/claude/hooks/save-session.ts"
         }
       ]
     }
@@ -221,7 +218,7 @@ Claude Code sends JSON data on stdin:
 - **Prompts user to save session**: Displays message asking if session should be saved
 - **Skips subagent sessions**: Silent exit for subagent sessions
 - **Works with `/save-session` command**: Integrates with session-manager plugin
-- **Documents session**: Creates summary in `~/.config/claude/history/sessions/YYYY-MM/YYYY-MM-DD-HHMMSS_SESSION_description.md`
+- **Documents session**: Creates summary in `~/.local/share/ai/sessions/YYYY-MM/YYYY-MM-DD-HHMMSS_SESSION_description.md`
 
 **Prompt Output:**
 ```
@@ -229,7 +226,7 @@ Claude Code sends JSON data on stdin:
 
 **Session ending**. Would you like me to save a summary of this session to your history?
 
-I can create a session entry in `~/.config/claude/history/sessions/` documenting:
+I can create a session entry in `~/.local/share/ai/sessions/` documenting:
 - What was accomplished
 - Decisions made
 - Next steps
@@ -412,7 +409,7 @@ YYYY-MM-DD-HHMMSS_TYPE_description.md
 
 **Directory Structure:**
 ```
-~/.config/claude/history/
+~/.local/share/ai/
 ├── sessions/
 │   └── 2024-01/
 │       ├── 2024-01-15_session-log.txt
@@ -527,10 +524,10 @@ func shouldDebounce() bool {
 cd /home/vincent/src/home
 
 # Build all hooks
-nix build .#claude-hooks
+# Old Nix build (removed)
 
 # Install to user profile
-nix profile install .#claude-hooks
+# No longer needed - hooks are TypeScript
 ```
 
 ### Adding to Home Manager
@@ -548,9 +545,9 @@ home.packages = with pkgs; [
 cd /home/vincent/src/home/tools/claude-hooks
 
 # Build all hooks
-go build -o bin/claude-hooks-initialize-session ./cmd/initialize-session
-go build -o bin/claude-hooks-capture-tool-output ./cmd/capture-tool-output
-go build -o bin/claude-hooks-save-session ./cmd/save-session
+# Old Go build: bun run ~/.config/claude/hooks/initialize-session.ts ./cmd/initialize-session
+# Old Go build: bun run ~/.config/claude/hooks/capture-tool-output.ts ./cmd/capture-tool-output
+# Old Go build: bun run ~/.config/claude/hooks/save-session.ts ./cmd/save-session
 
 # Copy to PATH
 sudo cp bin/* /usr/local/bin/
@@ -563,16 +560,16 @@ sudo cp bin/* /usr/local/bin/
 
 ```bash
 # Check binaries are in PATH
-which claude-hooks-initialize-session
-which claude-hooks-capture-tool-output
-which claude-hooks-save-session
+which bun run ~/.config/claude/hooks/initialize-session.ts
+which bun run ~/.config/claude/hooks/capture-tool-output.ts
+which bun run ~/.config/claude/hooks/save-session.ts
 
 # Test hooks manually
-claude-hooks-initialize-session
+bun run ~/.config/claude/hooks/initialize-session.ts
 
 # Test with input
 echo '{"tool_name":"Bash","tool_input":{},"tool_response":{},"conversation_id":"test"}' | \
-  claude-hooks-capture-tool-output
+  bun run ~/.config/claude/hooks/capture-tool-output.ts
 ```
 
 ### Configuration
@@ -587,7 +584,7 @@ Edit `/home/vincent/.config/claude/settings.json` to enable hooks (should alread
         "hooks": [
           {
             "type": "command",
-            "command": "claude-hooks-initialize-session"
+            "command": "bun run ~/.config/claude/hooks/initialize-session.ts"
           }
         ]
       }
@@ -597,7 +594,7 @@ Edit `/home/vincent/.config/claude/settings.json` to enable hooks (should alread
         "hooks": [
           {
             "type": "command",
-            "command": "claude-hooks-capture-tool-output"
+            "command": "bun run ~/.config/claude/hooks/capture-tool-output.ts"
           }
         ]
       }
@@ -607,7 +604,7 @@ Edit `/home/vincent/.config/claude/settings.json` to enable hooks (should alread
         "hooks": [
           {
             "type": "command",
-            "command": "claude-hooks-save-session"
+            "command": "bun run ~/.config/claude/hooks/save-session.ts"
           }
         ]
       }
@@ -627,9 +624,9 @@ Decide which event should trigger your hook (SessionStart, PostToolUse, etc.)
 
 ### Step 2: Create Hook Command
 
-**Option A: Go Implementation** (recommended)
+**Option A: TypeScript Implementation** (recommended)
 
-Create new command in `/home/vincent/src/home/tools/claude-hooks/cmd/`:
+Create new command in `~/.config/claude/hooks/ (source: dots/config/claude/hooks/)cmd/`:
 
 ```go
 package main
@@ -685,7 +682,7 @@ exit 0  # Always exit 0
 
 ```bash
 cd /home/vincent/src/home/tools/claude-hooks
-go build -o bin/my-custom-hook ./cmd/my-custom-hook
+# Old Go build: my-custom-hook ./cmd/my-custom-hook
 sudo cp bin/my-custom-hook /usr/local/bin/
 ```
 
@@ -781,7 +778,7 @@ f, _ := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
 ### Hook Not Running
 
 **Check:**
-1. Is hook binary in PATH? `which claude-hooks-initialize-session`
+1. Is hook binary in PATH? `which bun run ~/.config/claude/hooks/initialize-session.ts`
 2. Is path correct in settings.json? Use exact command name
 3. Is settings.json valid JSON? `jq . ~/.config/claude/settings.json`
 4. Did you restart Claude Code after editing settings.json?
@@ -789,10 +786,10 @@ f, _ := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
 **Debug:**
 ```bash
 # Test hook directly
-claude-hooks-initialize-session
+bun run ~/.config/claude/hooks/initialize-session.ts
 
 # Test with input
-echo '{"conversation_id":"test"}' | claude-hooks-capture-tool-output
+echo '{"conversation_id":"test"}' | bun run ~/.config/claude/hooks/capture-tool-output.ts
 
 # Check stderr output in terminal (hooks log there)
 ```
@@ -831,10 +828,10 @@ mkdir -p ~/.config/claude/history/sessions
 mkdir -p ~/.config/claude/history/tool-outputs
 
 # Check write permissions
-ls -la ~/.config/claude/history/
+ls -la ~/.local/share/ai/
 
 # Check binary permissions
-ls -la $(which claude-hooks-initialize-session)
+ls -la $(which bun run ~/.config/claude/hooks/initialize-session.ts)
 ```
 
 ---
@@ -849,7 +846,7 @@ ls -la $(which claude-hooks-initialize-session)
 **Debug:**
 ```bash
 # Check recent logs
-ls -lt ~/.config/claude/history/sessions/$(date +%Y-%m)/ | head -10
+ls -lt ~/.local/share/ai/sessions/$(date +%Y-%m)/ | head -10
 ls -lt ~/.config/claude/history/tool-outputs/$(date +%Y-%m)/ | head -10
 
 # Check if tool-outputs JSONL has entries
@@ -876,14 +873,14 @@ go build ./cmd/initialize-session
 **Nix Build:**
 ```bash
 cd /home/vincent/src/home
-nix build .#claude-hooks
+# Old Nix build (removed)
 
 # If vendorHash error, update in default.nix
 ```
 
 ---
 
-## Advantages of Go Implementation
+## Advantages of TypeScript Implementation
 
 1. **Zero runtime dependencies** - Compiled to native binary, no Bun/Node.js required
 2. **Faster execution** - Native code vs interpreted JavaScript
@@ -892,14 +889,14 @@ nix build .#claude-hooks
 5. **Nix integration** - Proper package management and reproducible builds
 6. **Type safety** - Compile-time checks without runtime overhead
 7. **Standard library** - Excellent built-in support for JSON, file I/O, paths
-8. **Smaller memory footprint** - Go binaries use less memory than Node.js processes
+8. **Smaller memory footprint** - TypeScript hooks use less memory than Node.js processes
 
 ---
 
 ## Related Documentation
 
-- **Hook Source Code:** `/home/vincent/src/home/tools/claude-hooks/`
-- **Nix Package:** `/home/vincent/src/home/tools/claude-hooks/default.nix`
+- **Hook Source Code:** `~/.config/claude/hooks/ (source: dots/config/claude/hooks/)`
+- **Nix Package:** `~/.config/claude/hooks/ (source: dots/config/claude/hooks/)default.nix`
 - **Configuration:** `/home/vincent/.config/claude/settings.json`
 - **History Directory:** `~/.config/claude/history/`
 
@@ -918,28 +915,28 @@ HOOK LIFECYCLE:
 
 KEY FILES:
 /home/vincent/.config/claude/settings.json        Hook configuration
-/home/vincent/src/home/tools/claude-hooks/        Hook source code
-~/.config/claude/history/sessions/                       Session logs
+~/.config/claude/hooks/ (source: dots/config/claude/hooks/)        Hook source code
+~/.local/share/ai/sessions/                       Session logs
 ~/.config/claude/history/tool-outputs/                   Tool output JSONL logs
 
 CONFIGURED HOOKS:
-claude-hooks-initialize-session    SessionStart - Set title, log session
-claude-hooks-capture-tool-output   PostToolUse - Log tool executions
-claude-hooks-save-session          SessionEnd - Prompt to save summary
+bun run ~/.config/claude/hooks/initialize-session.ts    SessionStart - Set title, log session
+bun run ~/.config/claude/hooks/capture-tool-output.ts   PostToolUse - Log tool executions
+bun run ~/.config/claude/hooks/save-session.ts          SessionEnd - Prompt to save summary
 
 VERIFY HOOKS:
-which claude-hooks-initialize-session
+which bun run ~/.config/claude/hooks/initialize-session.ts
 jq '.hooks' ~/.config/claude/settings.json
 ls -la ~/.config/claude/history/tool-outputs/
 
 DEBUGGING:
 # Test hooks manually
-claude-hooks-initialize-session
-echo '{"tool_name":"Bash","tool_input":{}}' | claude-hooks-capture-tool-output
+bun run ~/.config/claude/hooks/initialize-session.ts
+echo '{"tool_name":"Bash","tool_input":{}}' | bun run ~/.config/claude/hooks/capture-tool-output.ts
 
 # Check logs
 tail ~/.config/claude/history/tool-outputs/$(date +%Y-%m)/$(date +%Y-%m-%d)_tool-outputs.jsonl
-cat ~/.config/claude/history/sessions/$(date +%Y-%m)/$(date +%Y-%m-%d)_session-log.txt
+cat ~/.local/share/ai/sessions/$(date +%Y-%m)/$(date +%Y-%m-%d)_session-log.txt
 ```
 
 ---
dots/config/claude/skills/CORE/SKILL.md
@@ -102,7 +102,7 @@ Explicit permission to:
 ## File Organization
 
 - **Scratchpad** (`~/.config/claude/scratchpad/`) - Temporary files only. Delete when done.
-- **History** (`~/.config/claude/history/`) - Permanent valuable outputs.
+- **AI Storage** (`~/.local/share/ai/`) - Unified storage for sessions, research, plans, learnings (shared across all AI tools).
 - **Backups** (`~/.config/claude/history/backups/`) - All backups go here, NEVER inside skill directories.
 
 **Rules:**
@@ -157,24 +157,41 @@ You have EXPLICIT PERMISSION to say "I don't know" or "I'm not confident" when:
 
 **The Permission:** You will NEVER be penalized for honestly saying you don't know. Fabricating an answer is far worse than admitting uncertainty.
 
-## History System Quick Reference
+## Unified AI Storage Quick Reference
 
-**CRITICAL: When the user asks about ANYTHING done in the past, CHECK THE HISTORY SYSTEM FIRST.**
+**CRITICAL: When the user asks about ANYTHING done in the past, CHECK THE AI STORAGE FIRST.**
+
+**Location:** `~/.local/share/ai/` (shared across claude, pi, opencode)
 
 ```bash
 # Quick keyword search
-rg -i "keyword" ~/.config/claude/history/
+rg -i "keyword" ~/.local/share/ai/
 
 # List recent sessions
-ls -lt ~/.config/claude/history/sessions/2025-11/ | head -20
+ls -lt ~/.local/share/ai/sessions/$(date +%Y-%m)/ | head -20
+
+# List yesterday's sessions
+ls ~/.local/share/ai/sessions/$(date +%Y-%m)/$(date -d yesterday +%Y-%m-%d)-*.md
+
+# List research
+ls -lt ~/.local/share/ai/research/$(date +%Y-%m)/
+
+# List plans
+ls ~/.local/share/ai/plans/
 ```
 
 **Directory Reference:**
-- `history/sessions/YYYY-MM/` - Session summaries
-- `history/learnings/YYYY-MM/` - Problem-solving narratives
-- `history/research/YYYY-MM/` - Research & investigations
+- `sessions/YYYY-MM/YYYY-MM-DD-description.md` - Session summaries
+- `learnings/YYYY-MM/YYYY-MM-DD-description.md` - Problem-solving insights
+- `research/YYYY-MM/YYYY-MM-DD-description.md` - Research & investigations
+- `plans/description.md` - Project plans (no date prefix)
+- `sessions/YYYY-MM/YYYY-MM-DD_session-log.txt` - Daily session activity log
 
-**For complete history system documentation:** See `history-system.md`
+**Filename format:** `YYYY-MM-DD-description-with-dashes.md` (lowercase, hyphens only, no underscores)
+
+**NEVER write to:** `~/.config/claude/history/sessions/` (old location, blocked by hooks)
+
+**For complete documentation:** See `history-system.md`
 
 ---
 
@@ -234,8 +251,8 @@ For full skill list, see available skills in `~/.config/claude/skills/`
 **Example 2: User asks about past work**
 ```
 User: "What did we do with Tekton last week?"
-→ CORE reminds to check history system first
-→ Searches ~/.config/claude/history/sessions/
+→ CORE reminds to check AI storage first
+→ Searches ~/.local/share/ai/sessions/
 → Returns summary of work accomplished
 ```
 
dots/config/claude/README.org
@@ -84,13 +84,13 @@
 
 ** Hooks (hooks/)
 
-Event-driven automation implemented in Go (see =~/src/home/tools/claude-hooks=):
-- *claude-hooks-initialize-session* - SessionStart: Sets terminal title, logs session start
-- *claude-hooks-capture-tool-output* - PostToolUse: Logs tool executions to JSONL
-- *claude-hooks-save-session* - SessionEnd: Prompts to save session summary
-- *claude-hooks-validate-docs* - Manual/Pre-commit: Validates documentation links
+Event-driven automation in TypeScript (=~/.config/claude/hooks/=):
+- =initialize-session.ts= - SessionStart: Sets terminal title, logs session start
+- =capture-tool-output.ts= - PostToolUse: Logs tool executions to JSONL
+- =save-session.ts= - SessionEnd: Prompts to save session summary
+- =validate-write-path.ts= - PreToolUse: Validates file paths and filenames
 
-See =tools/claude-hooks/README.md= for detailed documentation.
+See =dots/config/claude/hooks/= for source code.
 
 ** Plugins (plugins/)
 
@@ -518,9 +518,9 @@
 cat ~/.config/claude/settings.json | grep -A 10 hooks
 
 # Verify hook binaries are in PATH
-which claude-hooks-initialize-session
-which claude-hooks-capture-tool-output
-which claude-hooks-save-session
+ls ~/.config/claude/hooks/initialize-session.ts
+ls ~/.config/claude/hooks/capture-tool-output.ts
+ls ~/.config/claude/hooks/save-session.ts
 #+end_src
 
 ** History not syncing?
@@ -536,7 +536,7 @@
 
 - *Agents*: See individual agent =.md= files in =agents/=
 - *Skills*: See each skill's =SKILL.md= in =skills/=
-- *Hooks*: See =~/src/home/tools/claude-hooks/README.md=
+- *Hooks*: See =~/.config/claude/hooks/= (TypeScript, run via bun)
 - *History System*: See =skills/CORE/history-system.md=
 - *Repository Instructions*: See =~/src/home/CLAUDE.md=
 - *PAI Implementation Plan*: See =~/desktop/org/notes/20251203T151822--personal-ai-infrastructure-implementation-plan__ai_claude_infrastructure_nixos_plan.org=
dots/config/claude/settings.json
@@ -7,7 +7,16 @@
         "hooks": [
           {
             "type": "command",
-            "command": "claude-hooks-validate-git-push"
+            "command": "bun run ~/.config/claude/hooks/validate-git-push.ts"
+          }
+        ]
+      },
+      {
+        "matcher": "Write",
+        "hooks": [
+          {
+            "type": "command",
+            "command": "bun run ~/.config/claude/hooks/validate-write-path.ts"
           }
         ]
       }
@@ -17,7 +26,7 @@
         "hooks": [
           {
             "type": "command",
-            "command": "claude-hooks-initialize-session"
+            "command": "bun run ~/.config/claude/hooks/initialize-session.ts"
           }
         ]
       }
@@ -27,11 +36,11 @@
         "hooks": [
           {
             "type": "command",
-            "command": "claude-hooks-capture-tool-output"
+            "command": "bun run ~/.config/claude/hooks/capture-tool-output.ts"
           },
           {
             "type": "command",
-            "command": "claude-hooks-update-terminal-title"
+            "command": "bun run ~/.config/claude/hooks/update-terminal-title.ts"
           }
         ]
       }
@@ -41,7 +50,7 @@
         "hooks": [
           {
             "type": "command",
-            "command": "/etc/profiles/per-user/vincent/bin/claude-hooks-save-session"
+            "command": "bun run ~/.config/claude/hooks/save-session.ts"
           }
         ]
       }
dots/config/copilot-hooks/claude-hooks.json
@@ -4,33 +4,38 @@
     "sessionStart": [
       {
         "type": "command",
-        "bash": "claude-hooks-initialize-session",
+        "bash": "bun run ~/.config/claude/hooks/initialize-session.ts",
         "timeoutSec": 5
       }
     ],
     "sessionEnd": [
       {
         "type": "command",
-        "bash": "claude-hooks-save-session",
+        "bash": "bun run ~/.config/claude/hooks/save-session.ts",
         "timeoutSec": 10
       }
     ],
     "preToolUse": [
       {
         "type": "command",
-        "bash": ".github/hooks/copilot-to-claude.sh claude-hooks-validate-git-push",
+        "bash": "bun run ~/.config/claude/hooks/validate-git-push.ts",
+        "timeoutSec": 5
+      },
+      {
+        "type": "command",
+        "bash": "bun run ~/.config/claude/hooks/validate-write-path.ts",
         "timeoutSec": 5
       }
     ],
     "postToolUse": [
       {
         "type": "command",
-        "bash": ".github/hooks/copilot-to-claude.sh claude-hooks-capture-tool-output",
+        "bash": "bun run ~/.config/claude/hooks/capture-tool-output.ts",
         "timeoutSec": 5
       },
       {
         "type": "command",
-        "bash": ".github/hooks/copilot-to-claude.sh claude-hooks-update-terminal-title",
+        "bash": "bun run ~/.config/claude/hooks/update-terminal-title.ts",
         "timeoutSec": 2
       }
     ]
dots/config/opencode/plugin/claude-hooks.ts
@@ -1,14 +1,14 @@
 /**
- * OpenCode plugin that wraps Claude Code hooks (Go binaries)
+ * OpenCode plugin that wraps Claude Code hooks (TypeScript hooks)
  *
- * This plugin provides compatibility with existing claude-hooks-* binaries
+ * This plugin provides compatibility with existing TypeScript hooks in ~/.config/claude/hooks/
  * by translating OpenCode's event format to Claude Code's JSON format.
  *
  * Hooks ported:
- * - tool.execute.before -> claude-hooks-validate-git-push
- * - tool.execute.after  -> claude-hooks-capture-tool-output, claude-hooks-update-terminal-title
- * - session.created     -> claude-hooks-initialize-session
- * - session.idle        -> claude-hooks-save-session (approximation of SessionEnd)
+ * - tool.execute.before -> bun run ~/.config/claude/hooks/validate-git-push.ts
+ * - tool.execute.after  -> bun run ~/.config/claude/hooks/capture-tool-output.ts, bun run ~/.config/claude/hooks/update-terminal-title.ts
+ * - session.created     -> bun run ~/.config/claude/hooks/initialize-session.ts
+ * - session.idle        -> bun run ~/.config/claude/hooks/save-session.ts (approximation of SessionEnd)
  */
 
 import type { Plugin } from "@opencode-ai/plugin"
@@ -58,12 +58,12 @@ export const ClaudeHooksPlugin: Plugin = async ({ $ }) => {
 
       try {
         const jsonInput = toClaudePreToolUse(input)
-        const result = await $`echo ${jsonInput} | claude-hooks-validate-git-push`.quiet()
+        const result = await $`echo ${jsonInput} | bun run ~/.config/claude/hooks/validate-git-push.ts`.quiet()
 
         if (result.exitCode !== 0) {
           // Block the command
           const stderr = result.stderr.toString().trim()
-          output.abort = stderr || "Blocked by claude-hooks-validate-git-push"
+          output.abort = stderr || "Blocked by bun run ~/.config/claude/hooks/validate-git-push.ts"
         }
       } catch (error) {
         // Don't block on hook errors - fail open
@@ -80,8 +80,8 @@ export const ClaudeHooksPlugin: Plugin = async ({ $ }) => {
 
         // Run both hooks in parallel (fire and forget)
         await Promise.allSettled([
-          $`echo ${jsonInput} | claude-hooks-capture-tool-output`.quiet(),
-          $`echo ${jsonInput} | claude-hooks-update-terminal-title`.quiet(),
+          $`echo ${jsonInput} | bun run ~/.config/claude/hooks/capture-tool-output.ts`.quiet(),
+          $`echo ${jsonInput} | bun run ~/.config/claude/hooks/update-terminal-title.ts`.quiet(),
         ])
       } catch (error) {
         // Silent failure - don't disrupt workflow
@@ -97,7 +97,7 @@ export const ClaudeHooksPlugin: Plugin = async ({ $ }) => {
       if (event.type === "session.created" && !sessionInitialized) {
         sessionInitialized = true
         try {
-          await $`claude-hooks-initialize-session`.quiet()
+          await $`bun run ~/.config/claude/hooks/initialize-session.ts`.quiet()
         } catch (error) {
           console.error("[claude-hooks] initialize-session error:", error)
         }
@@ -108,7 +108,7 @@ export const ClaudeHooksPlugin: Plugin = async ({ $ }) => {
       // Uncomment if you want save-session prompts on idle:
       // if (event.type === "session.idle") {
       //   try {
-      //     await $`claude-hooks-save-session`.quiet()
+      //     await $`bun run ~/.config/claude/hooks/save-session.ts`.quiet()
       //   } catch (error) {
       //     console.error("[claude-hooks] save-session error:", error)
       //   }
dots/Makefile
@@ -29,9 +29,10 @@ nvim : ~/.config/nvim
 all += claude-skills
 claude-skills : ~/.config/claude/skills
 
-all += claude-agents claude-settings claude-plugins claude-statusline claude-compat
+all += claude-agents claude-settings claude-hooks claude-plugins claude-statusline claude-compat
 claude-agents : ~/.config/claude/agents
 claude-settings : ~/.config/claude/settings.json
+claude-hooks : ~/.config/claude/hooks
 claude-plugins : ~/.config/claude/plugins/session-manager
 claude-statusline : ~/.config/claude/statusline.sh
 claude-compat : ~/.claude
pkgs/default.nix
@@ -17,7 +17,6 @@ in
   vrsync = pkgs.callPackage ./my/vrsync { };
   vde-thinkpad = pkgs.callPackage ./my/vde-thinkpad { };
   battery-monitor = pkgs.callPackage ../tools/battery-monitor { };
-  claude-hooks = pkgs.callPackage ../tools/claude-hooks { };
   ape = pkgs.callPackage ./ape { };
   ram = pkgs.callPackage ./ram { };
   govanityurl = pkgs.callPackage ./govanityurl { };
systems/aomi/home.nix
@@ -23,7 +23,6 @@
     go-org-readwise
     gh-pr
     lazypr
-    claude-hooks
     nixpkgs-pr-watch
     ssh-to-age
 
systems/kyushu/home.nix
@@ -77,7 +77,6 @@ in
     lazypr
     nixpkgs-pr-watch
     arr
-    claude-hooks
     toggle-color-scheme
     shpool-remote
     cliphist-cleanup
tools/claude-hooks/cmd/capture-tool-output/main.go
@@ -1,123 +0,0 @@
-package main
-
-import (
-	"encoding/json"
-	"fmt"
-	"io"
-	"os"
-	"path/filepath"
-	"time"
-
-	"github.com/vdemeester/home/tools/claude-hooks/internal/paths"
-)
-
-// ToolUseData represents the input from PostToolUse hook
-type ToolUseData struct {
-	ToolName       string                 `json:"tool_name"`
-	ToolInput      map[string]interface{} `json:"tool_input"`
-	ToolResponse   map[string]interface{} `json:"tool_response"`
-	ConversationID string                 `json:"conversation_id"`
-	Timestamp      string                 `json:"timestamp"`
-}
-
-// CaptureEntry represents a log entry in JSONL format
-type CaptureEntry struct {
-	Timestamp string                 `json:"timestamp"`
-	Tool      string                 `json:"tool"`
-	Input     map[string]interface{} `json:"input"`
-	Output    map[string]interface{} `json:"output"`
-	Session   string                 `json:"session"`
-}
-
-// List of tools to capture
-var interestingTools = map[string]bool{
-	"Bash":         true,
-	"Edit":         true,
-	"Write":        true,
-	"Read":         true,
-	"Task":         true,
-	"NotebookEdit": true,
-	"Skill":        true,
-	"SlashCommand": true,
-}
-
-func main() {
-	// Read input from stdin
-	input, err := io.ReadAll(os.Stdin)
-	if err != nil {
-		fmt.Fprintf(os.Stderr, "[capture-tool-output] Error reading stdin: %v\n", err)
-		os.Exit(0) // Silent failure - don't disrupt workflow
-	}
-
-	if len(input) == 0 {
-		os.Exit(0)
-	}
-
-	var data ToolUseData
-	if err := json.Unmarshal(input, &data); err != nil {
-		fmt.Fprintf(os.Stderr, "[capture-tool-output] Error parsing JSON: %v\n", err)
-		os.Exit(0) // Silent failure
-	}
-
-	// Only capture interesting tools
-	if !interestingTools[data.ToolName] {
-		os.Exit(0)
-	}
-
-	// Get today's date for organization
-	now := time.Now()
-	today := now.Format("2006-01-02")
-	yearMonth := now.Format("2006-01")
-
-	// Ensure capture directory exists
-	dateDir := filepath.Join(paths.HistoryDir(), "tool-outputs", yearMonth)
-	if err := os.MkdirAll(dateDir, 0755); err != nil {
-		fmt.Fprintf(os.Stderr, "[capture-tool-output] Error creating directory: %v\n", err)
-		os.Exit(0) // Silent failure
-	}
-
-	// Format output as JSONL
-	captureFile := filepath.Join(dateDir, fmt.Sprintf("%s_tool-outputs.jsonl", today))
-
-	timestamp := data.Timestamp
-	if timestamp == "" {
-		timestamp = now.Format(time.RFC3339)
-	}
-
-	entry := CaptureEntry{
-		Timestamp: timestamp,
-		Tool:      data.ToolName,
-		Input:     data.ToolInput,
-		Output:    data.ToolResponse,
-		Session:   data.ConversationID,
-	}
-
-	jsonData, err := json.Marshal(entry)
-	if err != nil {
-		fmt.Fprintf(os.Stderr, "[capture-tool-output] Error marshaling JSON: %v\n", err)
-		os.Exit(0) // Silent failure
-	}
-
-	// Append to daily log
-	f, err := os.OpenFile(captureFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
-	if err != nil {
-		fmt.Fprintf(os.Stderr, "[capture-tool-output] Error opening file: %v\n", err)
-		os.Exit(0) // Silent failure
-	}
-	defer f.Close()
-
-	if _, err := f.WriteString(string(jsonData) + "\n"); err != nil {
-		fmt.Fprintf(os.Stderr, "[capture-tool-output] Error writing to file: %v\n", err)
-		os.Exit(0) // Silent failure
-	}
-
-	// Desktop notifications disabled - they were too noisy
-	// notifyMsg := fmt.Sprintf("Tool executed: %s", data.ToolName)
-	// cmd := exec.Command("notify-send", "-u", "low", "Claude Hook", notifyMsg)
-	// if err := cmd.Run(); err != nil {
-	// 	// Silent failure - don't break workflow
-	// 	fmt.Fprintf(os.Stderr, "[capture-tool-output] Warning: Could not send notification: %v\n", err)
-	// }
-
-	os.Exit(0)
-}
tools/claude-hooks/cmd/initialize-session/main.go
@@ -1,174 +0,0 @@
-package main
-
-import (
-	"fmt"
-	"os"
-	"os/exec"
-	"path/filepath"
-	"strconv"
-	"strings"
-	"time"
-
-	"github.com/vdemeester/home/tools/claude-hooks/internal/paths"
-)
-
-const (
-	debounceDuration = 2 * time.Second
-)
-
-func getLockfile() string {
-	return filepath.Join(os.TempDir(), "claude-session-start.lock")
-}
-
-// shouldDebounce checks if we're within the debounce window
-func shouldDebounce() bool {
-	lockfile := getLockfile()
-
-	data, err := os.ReadFile(lockfile)
-	if err == nil {
-		lockTime, err := strconv.ParseInt(strings.TrimSpace(string(data)), 10, 64)
-		if err == nil {
-			now := time.Now().UnixMilli()
-			if now-lockTime < debounceDuration.Milliseconds() {
-				return true
-			}
-		}
-	}
-
-	// Update lockfile with current timestamp
-	now := time.Now().UnixMilli()
-	if err := os.WriteFile(lockfile, []byte(fmt.Sprintf("%d", now)), 0644); err != nil {
-		// Ignore write errors
-	}
-
-	return false
-}
-
-// isSubagentSession checks if this is a subagent session
-func isSubagentSession() bool {
-	claudeProjectDir := os.Getenv("CLAUDE_PROJECT_DIR")
-	if strings.Contains(claudeProjectDir, "/.claude/agents/") {
-		return true
-	}
-	if os.Getenv("CLAUDE_AGENT_TYPE") != "" {
-		return true
-	}
-	return false
-}
-
-// setTerminalTitle sets the terminal tab title using ANSI escape codes
-func setTerminalTitle(title string) {
-	fmt.Fprintf(os.Stderr, "\x1b]0;%s\x07", title)
-	fmt.Fprintf(os.Stderr, "\x1b]2;%s\x07", title)
-	fmt.Fprintf(os.Stderr, "\x1b]30;%s\x07", title)
-}
-
-// logSessionStart logs the session start to history
-func logSessionStart() error {
-	timestamp := paths.GetTimestamp()
-	yearMonth := timestamp[:7] // YYYY-MM
-
-	logDir := filepath.Join(paths.HistoryDir(), "sessions", yearMonth)
-	if err := os.MkdirAll(logDir, 0755); err != nil {
-		return err
-	}
-
-	logEntry := fmt.Sprintf("%s - Session started\n", time.Now().Format(time.RFC3339))
-	logFile := filepath.Join(logDir, fmt.Sprintf("%s_session-log.txt", timestamp[:10]))
-
-	f, err := os.OpenFile(logFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
-	if err != nil {
-		return err
-	}
-	defer f.Close()
-
-	_, err = f.WriteString(logEntry)
-	return err
-}
-
-// loadCoreSkill outputs the CORE skill content so Claude receives it at session start
-func loadCoreSkill() error {
-	homeDir, err := os.UserHomeDir()
-	if err != nil {
-		return err
-	}
-
-	coreSkillPath := filepath.Join(homeDir, ".config/claude/skills/CORE/SKILL.md")
-	content, err := os.ReadFile(coreSkillPath)
-	if err != nil {
-		return err
-	}
-
-	// Output to stdout so Claude receives it
-	fmt.Println("\n<!-- CORE Skill Auto-Loaded at Session Start -->")
-	fmt.Println(string(content))
-	fmt.Println("<!-- End CORE Skill -->")
-
-	return nil
-}
-
-// isPiContext checks if we're running under pi coding agent
-func isPiContext() bool {
-	// Strategy 1: Check immediate parent process
-	ppid := os.Getppid()
-	cmdline := fmt.Sprintf("/proc/%d/comm", ppid)
-	data, err := os.ReadFile(cmdline)
-	if err == nil && strings.TrimSpace(string(data)) == "pi" {
-		return true
-	}
-
-	// Strategy 2: Check if "pi" is anywhere in the process tree
-	cmd := exec.Command("sh", "-c", fmt.Sprintf("ps -o comm= -p $(ps -o ppid= -p %d) 2>/dev/null || pstree -s %d 2>/dev/null", ppid, os.Getpid()))
-	output, err := cmd.Output()
-	if err == nil && strings.Contains(string(output), "pi") {
-		return true
-	}
-
-	// Strategy 3: Check PI_* environment variables (if pi sets any in future)
-	if os.Getenv("PI_AGENT") != "" || os.Getenv("PI_CODING_AGENT") != "" {
-		return true
-	}
-
-	return false
-}
-
-func main() {
-	// Check if this is a subagent session
-	if isSubagentSession() {
-		fmt.Fprintln(os.Stderr, "🤖 Subagent session detected - skipping session initialization")
-		os.Exit(0)
-	}
-
-	// Check debounce to prevent duplicate notifications
-	if shouldDebounce() {
-		fmt.Fprintln(os.Stderr, "🔇 Debouncing duplicate SessionStart event")
-		os.Exit(0)
-	}
-
-	// Detect context and set appropriate title
-	isPi := isPiContext()
-	tabTitle := "Claude Ready"
-	if isPi {
-		tabTitle = "π Ready"
-	}
-
-	setTerminalTitle(tabTitle)
-	fmt.Fprintf(os.Stderr, "📍 Session initialized: \"%s\"\n", tabTitle)
-
-	// Load CORE skill at session start
-	if err := loadCoreSkill(); err != nil {
-		// Warn but don't break session start
-		fmt.Fprintf(os.Stderr, "[initialize-session] Warning: Could not load CORE skill: %v\n", err)
-	}
-
-	// Ring terminal bell to notify user (works with kitty bell_on_tab)
-	fmt.Fprint(os.Stderr, "\a")
-
-	// Log session start to history (silent failure)
-	if err := logSessionStart(); err != nil {
-		// Don't break session start for logging issues
-		fmt.Fprintf(os.Stderr, "[initialize-session] Warning: Could not log session start: %v\n", err)
-	}
-
-	os.Exit(0)
-}
tools/claude-hooks/cmd/save-session/main.go
@@ -1,72 +0,0 @@
-package main
-
-import (
-	"fmt"
-	"os"
-	"os/exec"
-)
-
-// isSubagentSession checks if this is a subagent session
-func isSubagentSession() bool {
-	claudeProjectDir := os.Getenv("CLAUDE_PROJECT_DIR")
-	if len(claudeProjectDir) > 0 && (len(claudeProjectDir) < 2 || claudeProjectDir[len(claudeProjectDir)-2:] != "/.") {
-		return true
-	}
-	if os.Getenv("CLAUDE_AGENT_TYPE") != "" {
-		return true
-	}
-	return false
-}
-
-func main() {
-	// Check if this is a subagent session
-	if isSubagentSession() {
-		// Silent exit for subagent sessions
-		os.Exit(0)
-	}
-
-	// Ring terminal bell to notify user (works with kitty bell_on_tab)
-	fmt.Fprint(os.Stderr, "\a")
-
-	// Get session statistics
-	statsCmd := exec.Command("claude-hooks-session-stats")
-	statsCmd.Env = os.Environ()
-	statsOutput, err := statsCmd.Output()
-	if err != nil {
-		// Continue without stats if tool fails
-		fmt.Fprintf(os.Stderr, "[save-session] Warning: Could not get session stats: %v\n", err)
-	}
-
-	// Output directive for Claude to save the session
-	// This will be shown in the conversation
-	fmt.Println("")
-	fmt.Println("---")
-	fmt.Println("")
-	fmt.Println("# Automatic Session Summary")
-	fmt.Println("")
-	fmt.Println("**IMPORTANT**: Please create a session summary and save it to the history directory.")
-	fmt.Println("")
-
-	// Show statistics if available
-	if len(statsOutput) > 0 {
-		fmt.Print(string(statsOutput))
-	}
-
-	fmt.Println("## Instructions")
-	fmt.Println("")
-	fmt.Println("Create a brief summary (2-4 paragraphs) of this session documenting:")
-	fmt.Println("- Primary tasks accomplished")
-	fmt.Println("- Key decisions or solutions")
-	fmt.Println("- Files/systems modified")
-	fmt.Println("- Any remaining work or next steps")
-	fmt.Println("")
-	fmt.Println("Save the summary using the Write tool to:")
-	fmt.Println("`~/.config/claude/history/sessions/<YYYY-MM>/<YYYY-MM-DD>_<brief-slug>.md`")
-	fmt.Println("")
-	fmt.Println("Use the format: `YYYY-MM-DD_<2-4 word slug describing the session>.md`")
-	fmt.Println("")
-	fmt.Println("Example: `~/.config/claude/history/sessions/2025-12/2025-12-10_notification-filtering-arr-completion.md`")
-	fmt.Println("")
-
-	os.Exit(0)
-}
tools/claude-hooks/cmd/session-stats/main.go
@@ -1,231 +0,0 @@
-package main
-
-import (
-	"bufio"
-	"encoding/json"
-	"fmt"
-	"os"
-	"path/filepath"
-	"sort"
-	"strings"
-	"time"
-
-	"github.com/vdemeester/home/tools/claude-hooks/internal/paths"
-)
-
-// CaptureEntry represents a log entry from tool-outputs JSONL
-type CaptureEntry struct {
-	Timestamp string                 `json:"timestamp"`
-	Tool      string                 `json:"tool"`
-	Input     map[string]interface{} `json:"input"`
-	Output    map[string]interface{} `json:"output"`
-	Session   string                 `json:"session"`
-}
-
-// SessionStats holds aggregated statistics
-type SessionStats struct {
-	ToolCounts    map[string]int
-	FilesModified []string
-	FilesRead     []string
-	CommandsRun   []string
-	StartTime     time.Time
-	EndTime       time.Time
-	Duration      time.Duration
-}
-
-func main() {
-	conversationID := os.Getenv("CLAUDE_CONVERSATION_ID")
-
-	// Get today's tool output file
-	now := time.Now()
-	today := now.Format("2006-01-02")
-	yearMonth := now.Format("2006-01")
-
-	toolOutputFile := filepath.Join(
-		paths.HistoryDir(),
-		"tool-outputs",
-		yearMonth,
-		fmt.Sprintf("%s_tool-outputs.jsonl", today),
-	)
-
-	stats := &SessionStats{
-		ToolCounts:    make(map[string]int),
-		FilesModified: []string{},
-		FilesRead:     []string{},
-		CommandsRun:   []string{},
-	}
-
-	// Read and parse JSONL
-	f, err := os.Open(toolOutputFile)
-	if err != nil {
-		// Silent failure - file might not exist for new sessions
-		fmt.Fprintln(os.Stderr, "[session-stats] No tool output file found")
-		os.Exit(0)
-	}
-	defer f.Close()
-
-	scanner := bufio.NewScanner(f)
-	// Increase buffer size for large lines
-	buf := make([]byte, 0, 64*1024)
-	scanner.Buffer(buf, 1024*1024)
-
-	firstEntry := true
-	for scanner.Scan() {
-		var entry CaptureEntry
-		if err := json.Unmarshal(scanner.Bytes(), &entry); err != nil {
-			continue // Skip malformed lines
-		}
-
-		// Only count entries from this session if we have a conversation ID
-		if conversationID != "" && entry.Session != "" && entry.Session != conversationID {
-			continue
-		}
-
-		stats.ToolCounts[entry.Tool]++
-
-		// Track timestamps
-		if t, err := time.Parse(time.RFC3339, entry.Timestamp); err == nil {
-			if firstEntry {
-				stats.StartTime = t
-				firstEntry = false
-			}
-			stats.EndTime = t
-		}
-
-		// Extract file paths and commands based on tool type
-		switch entry.Tool {
-		case "Edit", "Write":
-			if path, ok := entry.Input["file_path"].(string); ok {
-				if !contains(stats.FilesModified, path) {
-					stats.FilesModified = append(stats.FilesModified, path)
-				}
-			}
-		case "Read":
-			if path, ok := entry.Input["file_path"].(string); ok {
-				if !contains(stats.FilesRead, path) {
-					stats.FilesRead = append(stats.FilesRead, path)
-				}
-			}
-		case "Bash":
-			if cmd, ok := entry.Input["command"].(string); ok {
-				// Only include interesting commands (not trivial ones)
-				if isInterestingCommand(cmd) {
-					// Truncate long commands
-					if len(cmd) > 80 {
-						cmd = cmd[:77] + "..."
-					}
-					stats.CommandsRun = append(stats.CommandsRun, cmd)
-				}
-			}
-		}
-	}
-
-	if err := scanner.Err(); err != nil {
-		fmt.Fprintf(os.Stderr, "[session-stats] Error reading file: %v\n", err)
-	}
-
-	// Calculate duration
-	if !stats.StartTime.IsZero() && !stats.EndTime.IsZero() {
-		stats.Duration = stats.EndTime.Sub(stats.StartTime)
-	}
-
-	// Output statistics
-	printStats(stats)
-}
-
-func contains(slice []string, item string) bool {
-	for _, s := range slice {
-		if s == item {
-			return true
-		}
-	}
-	return false
-}
-
-func isInterestingCommand(cmd string) bool {
-	// Skip trivial commands
-	boring := []string{
-		"ls ", "pwd", "echo ", "cat ", "head ", "tail ",
-		"git status", "git diff", "git log",
-	}
-
-	cmdLower := strings.ToLower(cmd)
-	for _, b := range boring {
-		if strings.HasPrefix(cmdLower, b) {
-			return false
-		}
-	}
-
-	return true
-}
-
-func printStats(stats *SessionStats) {
-	fmt.Println("## Session Statistics")
-	fmt.Println()
-
-	// Duration
-	if stats.Duration > 0 {
-		fmt.Printf("**Duration:** %s\n", stats.Duration.Round(time.Second))
-		fmt.Println()
-	}
-
-	// Tool usage
-	if len(stats.ToolCounts) > 0 {
-		fmt.Println("**Tools used:**")
-
-		// Sort tools by count
-		type toolCount struct {
-			tool  string
-			count int
-		}
-		var tools []toolCount
-		for tool, count := range stats.ToolCounts {
-			tools = append(tools, toolCount{tool, count})
-		}
-		sort.Slice(tools, func(i, j int) bool {
-			return tools[i].count > tools[j].count
-		})
-
-		for _, tc := range tools {
-			fmt.Printf("- %s: %d\n", tc.tool, tc.count)
-		}
-		fmt.Println()
-	}
-
-	// Files modified
-	if len(stats.FilesModified) > 0 {
-		fmt.Println("**Files modified:**")
-		for _, f := range stats.FilesModified {
-			// Shorten home directory paths
-			f = strings.Replace(f, os.Getenv("HOME"), "~", 1)
-			fmt.Printf("- %s\n", f)
-		}
-		fmt.Println()
-	}
-
-	// Notable commands (limit to 10)
-	if len(stats.CommandsRun) > 0 {
-		fmt.Println("**Commands executed:**")
-		limit := len(stats.CommandsRun)
-		if limit > 10 {
-			limit = 10
-		}
-		for i := 0; i < limit; i++ {
-			fmt.Printf("- `%s`\n", stats.CommandsRun[i])
-		}
-		if len(stats.CommandsRun) > 10 {
-			fmt.Printf("- ... and %d more\n", len(stats.CommandsRun)-10)
-		}
-		fmt.Println()
-	}
-
-	// File reads (only if not too many)
-	if len(stats.FilesRead) > 0 && len(stats.FilesRead) <= 15 {
-		fmt.Println("**Files read:**")
-		for _, f := range stats.FilesRead {
-			f = strings.Replace(f, os.Getenv("HOME"), "~", 1)
-			fmt.Printf("- %s\n", f)
-		}
-		fmt.Println()
-	}
-}
tools/claude-hooks/cmd/update-terminal-title/main.go
@@ -1,191 +0,0 @@
-package main
-
-import (
-	"encoding/json"
-	"fmt"
-	"os"
-	"os/exec"
-	"path/filepath"
-	"strings"
-)
-
-// TodoItem represents a single TODO item from TodoWrite
-type TodoItem struct {
-	Content    string `json:"content"`
-	Status     string `json:"status"`
-	ActiveForm string `json:"activeForm"`
-}
-
-// ToolResult represents the structure of tool execution results from PostToolUse hook
-type ToolResult struct {
-	ToolName  string          `json:"tool_name"`
-	ToolInput json.RawMessage `json:"tool_input"`
-}
-
-// TodoWriteParams represents TodoWrite tool parameters
-type TodoWriteParams struct {
-	Todos []TodoItem `json:"todos"`
-}
-
-// SkillParams represents Skill tool parameters
-type SkillParams struct {
-	Skill string `json:"skill"`
-}
-
-// setTerminalTitle sets the terminal tab title using ANSI escape codes
-func setTerminalTitle(title string) {
-	fmt.Fprintf(os.Stderr, "\x1b]0;%s\x07", title)
-	fmt.Fprintf(os.Stderr, "\x1b]2;%s\x07", title)
-	fmt.Fprintf(os.Stderr, "\x1b]30;%s\x07", title)
-}
-
-// getProjectName extracts the project name from CLAUDE_PROJECT_DIR
-func getProjectName() string {
-	projectDir := os.Getenv("CLAUDE_PROJECT_DIR")
-	if projectDir == "" {
-		return ""
-	}
-	return filepath.Base(projectDir)
-}
-
-// getSkillIcon returns an emoji icon for known skills
-func getSkillIcon(skillName string) string {
-	icons := map[string]string{
-		"Journal":    "📓",
-		"Notes":      "📝",
-		"TODOs":      "✅",
-		"Org":        "📋",
-		"Git":        "🔧",
-		"GitHub":     "🐙",
-		"Email":      "📧",
-		"Python":     "🐍",
-		"golang":     "🐹",
-		"Rust":       "🦀",
-		"Nix":        "❄️",
-		"Kubernetes": "☸️",
-		"Tekton":     "🚀",
-	}
-
-	if icon, ok := icons[skillName]; ok {
-		return icon
-	}
-	return "🤖"
-}
-
-// getTodoProgress extracts task progress from TodoWrite parameters
-func getTodoProgress(todos []TodoItem) (string, bool) {
-	var inProgressTask string
-	completedCount := 0
-	total := len(todos)
-
-	if total == 0 {
-		return "", false
-	}
-
-	for _, todo := range todos {
-		if todo.Status == "completed" {
-			completedCount++
-		} else if todo.Status == "in_progress" {
-			inProgressTask = todo.ActiveForm
-		}
-	}
-
-	if inProgressTask != "" {
-		progress := fmt.Sprintf("[%d/%d] %s", completedCount+1, total, inProgressTask)
-		return progress, true
-	}
-
-	return "", false
-}
-
-// isPiContext checks if we're running under pi coding agent
-func isPiContext() bool {
-	// Strategy 1: Check immediate parent process
-	ppid := os.Getppid()
-	cmdline := fmt.Sprintf("/proc/%d/comm", ppid)
-	data, err := os.ReadFile(cmdline)
-	if err == nil && strings.TrimSpace(string(data)) == "pi" {
-		return true
-	}
-
-	// Strategy 2: Check if "pi" is anywhere in the process tree
-	// Look at process tree via pstree or ps
-	cmd := exec.Command("sh", "-c", fmt.Sprintf("ps -o comm= -p $(ps -o ppid= -p %d) 2>/dev/null || pstree -s %d 2>/dev/null", ppid, os.Getpid()))
-	output, err := cmd.Output()
-	if err == nil && strings.Contains(string(output), "pi") {
-		return true
-	}
-
-	// Strategy 3: Check PI_* environment variables (if pi sets any in future)
-	if os.Getenv("PI_AGENT") != "" || os.Getenv("PI_CODING_AGENT") != "" {
-		return true
-	}
-
-	return false
-}
-
-// buildTitle constructs the terminal title based on available context
-func buildTitle(toolResult *ToolResult) string {
-	var parts []string
-	isPi := isPiContext()
-
-	// Determine prefix (π for pi, Claude for Claude Code)
-	prefix := "Claude"
-	if isPi {
-		prefix = "π"
-	}
-
-	// Priority 1: Check for active task from TodoWrite
-	if toolResult != nil && toolResult.ToolName == "TodoWrite" {
-		var params TodoWriteParams
-		if err := json.Unmarshal(toolResult.ToolInput, &params); err == nil {
-			if progress, ok := getTodoProgress(params.Todos); ok {
-				parts = append(parts, progress)
-			}
-		}
-	}
-
-	// Priority 2: Check for active skill
-	if toolResult != nil && toolResult.ToolName == "Skill" {
-		var params SkillParams
-		if err := json.Unmarshal(toolResult.ToolInput, &params); err == nil {
-			icon := getSkillIcon(params.Skill)
-			parts = append(parts, fmt.Sprintf("%s %s", icon, params.Skill))
-		}
-	}
-
-	// Priority 3: Add project context
-	if projectName := getProjectName(); projectName != "" {
-		parts = append(parts, projectName)
-	}
-
-	// Priority 4: Always add working directory
-	if cwd, err := os.Getwd(); err == nil {
-		parts = append(parts, filepath.Base(cwd))
-	}
-
-	// Build final title
-	if len(parts) > 0 {
-		return fmt.Sprintf("%s %s", prefix, strings.Join(parts, " • "))
-	}
-
-	return fmt.Sprintf("%s Ready", prefix)
-}
-
-func main() {
-	// Read tool result from stdin (if provided by hook system)
-	var toolResult ToolResult
-	decoder := json.NewDecoder(os.Stdin)
-
-	// Try to decode, but don't fail if stdin is empty
-	if err := decoder.Decode(&toolResult); err != nil {
-		// No tool result provided, just use project context
-		title := buildTitle(nil)
-		setTerminalTitle(title)
-		return
-	}
-
-	// Build and set title based on tool result
-	title := buildTitle(&toolResult)
-	setTerminalTitle(title)
-}
tools/claude-hooks/cmd/validate-docs/main.go
@@ -1,169 +0,0 @@
-package main
-
-import (
-	"bufio"
-	"fmt"
-	"os"
-	"path/filepath"
-	"regexp"
-	"strings"
-
-	"github.com/bmatcuk/doublestar/v4"
-)
-
-// ANSI color codes
-const (
-	colorReset  = "\x1b[0m"
-	colorRed    = "\x1b[31m"
-	colorGreen  = "\x1b[32m"
-	colorYellow = "\x1b[33m"
-	colorCyan   = "\x1b[36m"
-)
-
-type brokenLink struct {
-	file   string
-	link   string
-	target string
-	line   int
-}
-
-// extractLinks extracts markdown links from content
-func extractLinks(content string) []struct {
-	link string
-	line int
-} {
-	var links []struct {
-		link string
-		line int
-	}
-
-	// Match [text](path) style links
-	linkRegex := regexp.MustCompile(`\[([^\]]+)\]\(([^)]+)\)`)
-
-	scanner := bufio.NewScanner(strings.NewReader(content))
-	lineNum := 0
-	for scanner.Scan() {
-		lineNum++
-		line := scanner.Text()
-
-		matches := linkRegex.FindAllStringSubmatch(line, -1)
-		for _, match := range matches {
-			if len(match) >= 3 {
-				link := match[2]
-
-				// Skip external URLs, anchors, and mailto links
-				if strings.HasPrefix(link, "http://") ||
-					strings.HasPrefix(link, "https://") ||
-					strings.HasPrefix(link, "#") ||
-					strings.HasPrefix(link, "mailto:") {
-					continue
-				}
-
-				links = append(links, struct {
-					link string
-					line int
-				}{link: link, line: lineNum})
-			}
-		}
-	}
-
-	return links
-}
-
-// resolveLink resolves a link path relative to the file
-func resolveLink(fromFile, linkPath, baseDir string) string {
-	// Remove anchor if present
-	parts := strings.Split(linkPath, "#")
-	pathWithoutAnchor := parts[0]
-
-	// If it starts with ~, expand to home directory
-	if strings.HasPrefix(pathWithoutAnchor, "~/") {
-		home, err := os.UserHomeDir()
-		if err != nil {
-			return pathWithoutAnchor
-		}
-		return filepath.Join(home, pathWithoutAnchor[2:])
-	}
-
-	// If it's absolute, use as-is
-	if filepath.IsAbs(pathWithoutAnchor) {
-		return pathWithoutAnchor
-	}
-
-	// Otherwise, resolve relative to the file's directory
-	fileDir := filepath.Dir(fromFile)
-	return filepath.Join(fileDir, pathWithoutAnchor)
-}
-
-// validateDocs validates markdown files in a directory
-func validateDocs(baseDir string) []brokenLink {
-	var broken []brokenLink
-
-	// Find all markdown files using doublestar glob
-	pattern := filepath.Join(baseDir, "**/*.md")
-	matches, err := doublestar.FilepathGlob(pattern)
-	if err != nil {
-		fmt.Fprintf(os.Stderr, "%sError globbing files: %v%s\n", colorYellow, err, colorReset)
-		return broken
-	}
-
-	for _, filePath := range matches {
-		// Skip node_modules and hidden directories
-		if strings.Contains(filePath, "node_modules") || strings.Contains(filePath, "/.") {
-			continue
-		}
-
-		content, err := os.ReadFile(filePath)
-		if err != nil {
-			fmt.Fprintf(os.Stderr, "%sWarning: Could not read %s%s\n", colorYellow, filePath, colorReset)
-			continue
-		}
-
-		links := extractLinks(string(content))
-		for _, linkInfo := range links {
-			targetPath := resolveLink(filePath, linkInfo.link, baseDir)
-
-			// Check if target exists
-			if _, err := os.Stat(targetPath); os.IsNotExist(err) {
-				// Get relative path for display
-				relPath, _ := filepath.Rel(baseDir, filePath)
-				broken = append(broken, brokenLink{
-					file:   relPath,
-					link:   linkInfo.link,
-					target: targetPath,
-					line:   linkInfo.line,
-				})
-			}
-		}
-	}
-
-	return broken
-}
-
-func main() {
-	baseDir, err := os.Getwd()
-	if err != nil {
-		fmt.Fprintf(os.Stderr, "Error getting current directory: %v\n", err)
-		os.Exit(1)
-	}
-
-	fmt.Printf("\n%s🔍 Documentation Link Validator%s\n", colorCyan, colorReset)
-	fmt.Printf("%s   Base directory: %s%s\n\n", colorCyan, baseDir, colorReset)
-
-	brokenLinks := validateDocs(baseDir)
-
-	if len(brokenLinks) > 0 {
-		fmt.Printf("\n%s❌ Found %d broken link(s):%s\n\n", colorRed, len(brokenLinks), colorReset)
-
-		for _, broken := range brokenLinks {
-			fmt.Printf("  %s%s:%d%s\n", colorYellow, broken.file, broken.line, colorReset)
-			fmt.Printf("    → %s%s%s (not found)\n\n", colorRed, broken.link, colorReset)
-		}
-
-		fmt.Printf("\n%sDocumentation validation failed. Please fix the broken links.%s\n\n", colorRed, colorReset)
-		os.Exit(1)
-	}
-
-	fmt.Printf("%s✅ All documentation links are valid%s\n\n", colorGreen, colorReset)
-	os.Exit(0)
-}
tools/claude-hooks/cmd/validate-git-push/main.go
@@ -1,143 +0,0 @@
-package main
-
-import (
-	"encoding/json"
-	"fmt"
-	"io"
-	"os"
-	"regexp"
-	"strings"
-)
-
-// PreToolUseData represents the input from PreToolUse hook
-type PreToolUseData struct {
-	ToolName       string                 `json:"tool_name"`
-	ToolInput      map[string]interface{} `json:"tool_input"`
-	ConversationID string                 `json:"conversation_id"`
-}
-
-// Check if a git push command uses explicit refspec (branch:branch)
-func hasExplicitRefspec(command string) bool {
-	// Match patterns like:
-	// git push origin branch:branch
-	// git push origin HEAD:branch
-	// git push -u origin branch:branch
-	// git push --force-with-lease origin branch:branch
-	refspecPattern := regexp.MustCompile(`git\s+push\s+.*\s+\S+:\S+`)
-	return refspecPattern.MatchString(command)
-}
-
-// Check if this is a dangerous git add command
-func isDangerousGitAdd(command string) bool {
-	// Match patterns like:
-	// git add -A
-	// git add --all
-	// git add .
-	dangerousAddPatterns := []*regexp.Regexp{
-		regexp.MustCompile(`(^|&&|\|\||;|\||\$\()\s*git\s+add\s+-A(\s|$)`),
-		regexp.MustCompile(`(^|&&|\|\||;|\||\$\()\s*git\s+add\s+--all(\s|$)`),
-		regexp.MustCompile(`(^|&&|\|\||;|\||\$\()\s*git\s+add\s+\.(\s|$)`),
-	}
-
-	for _, pattern := range dangerousAddPatterns {
-		if pattern.MatchString(command) {
-			return true
-		}
-	}
-	return false
-}
-
-// Check if this is a git push command (not just the string "git push" inside arguments)
-func isGitPush(command string) bool {
-	// Match git push only when it appears as an actual command:
-	// - At the start of the command
-	// - After command separators: && || ; |
-	// - After $( for command substitution
-	// This avoids false positives on "git push" inside heredocs, strings, or commit messages
-	gitPushPattern := regexp.MustCompile(`(^|&&|\|\||;|\||\$\()\s*git\s+push(\s|$)`)
-	return gitPushPattern.MatchString(command)
-}
-
-// Check if pushing to a protected branch without explicit refspec
-func isPushToProtectedBranch(command string) bool {
-	// These patterns indicate pushing to main/master without explicit refspec
-	protectedBranches := []string{":main", ":master"}
-	for _, branch := range protectedBranches {
-		if strings.Contains(command, branch) {
-			return true
-		}
-	}
-	return false
-}
-
-func main() {
-	// Read input from stdin
-	input, err := io.ReadAll(os.Stdin)
-	if err != nil {
-		fmt.Fprintf(os.Stderr, "[validate-git-push] Error reading stdin: %v\n", err)
-		os.Exit(0) // Allow on error - don't block workflow
-	}
-
-	if len(input) == 0 {
-		os.Exit(0)
-	}
-
-	var data PreToolUseData
-	if err := json.Unmarshal(input, &data); err != nil {
-		fmt.Fprintf(os.Stderr, "[validate-git-push] Error parsing JSON: %v\n", err)
-		os.Exit(0) // Allow on error
-	}
-
-	// Only check Bash tool
-	if data.ToolName != "Bash" {
-		os.Exit(0)
-	}
-
-	// Get the command
-	command, ok := data.ToolInput["command"].(string)
-	if !ok || command == "" {
-		os.Exit(0)
-	}
-
-	// Check for dangerous git add commands first
-	if isDangerousGitAdd(command) {
-		fmt.Fprintln(os.Stderr, "BLOCKED: Dangerous git add command detected!")
-		fmt.Fprintln(os.Stderr, "")
-		fmt.Fprintln(os.Stderr, "Commands like 'git add -A', 'git add --all', and 'git add .' can add unintended files.")
-		fmt.Fprintln(os.Stderr, "")
-		fmt.Fprintln(os.Stderr, "Use explicit file paths instead:")
-		fmt.Fprintln(os.Stderr, "  git add path/to/specific/file.txt")
-		fmt.Fprintln(os.Stderr, "  git add path/to/directory/")
-		fmt.Fprintln(os.Stderr, "  git add *.go  # for specific patterns")
-		fmt.Fprintln(os.Stderr, "")
-		fmt.Fprintf(os.Stderr, "Blocked command: %s\n", command)
-		os.Exit(2) // Non-zero exit blocks the tool
-	}
-
-	// Check if this is a git push command
-	if !isGitPush(command) {
-		os.Exit(0)
-	}
-
-	// Check if it has explicit refspec
-	if !hasExplicitRefspec(command) {
-		// Block the command - output error message to stderr and exit non-zero
-		fmt.Fprintln(os.Stderr, "BLOCKED: git push without explicit refspec detected!")
-		fmt.Fprintln(os.Stderr, "")
-		fmt.Fprintln(os.Stderr, "The command uses implicit branch tracking which can push to wrong branches.")
-		fmt.Fprintln(os.Stderr, "")
-		fmt.Fprintln(os.Stderr, "Use explicit refspec instead:")
-		fmt.Fprintln(os.Stderr, "  git push origin <branch>:<branch>")
-		fmt.Fprintln(os.Stderr, "  git push origin HEAD:<branch>")
-		fmt.Fprintln(os.Stderr, "")
-		fmt.Fprintf(os.Stderr, "Blocked command: %s\n", command)
-		os.Exit(2) // Non-zero exit blocks the tool
-	}
-
-	// Warn about pushing to protected branches (but allow it with explicit refspec)
-	if isPushToProtectedBranch(command) {
-		fmt.Fprintf(os.Stderr, "[validate-git-push] Warning: Pushing to protected branch (main/master)\n")
-	}
-
-	os.Exit(0) // Allow the command
-}
tools/claude-hooks/internal/paths/paths.go
@@ -1,79 +0,0 @@
-package paths
-
-import (
-	"fmt"
-	"os"
-	"path/filepath"
-	"time"
-)
-
-// ClaudeDir returns the base Claude directory (~/.claude or CLAUDE_DIR env var)
-func ClaudeDir() string {
-	if dir := os.Getenv("CLAUDE_DIR"); dir != "" {
-		return dir
-	}
-	home, err := os.UserHomeDir()
-	if err != nil {
-		fmt.Fprintf(os.Stderr, "Error getting home directory: %v\n", err)
-		os.Exit(1)
-	}
-	return filepath.Join(home, ".claude")
-}
-
-// HooksDir returns the hooks directory
-func HooksDir() string {
-	return filepath.Join(ClaudeDir(), "hooks")
-}
-
-// SkillsDir returns the skills directory
-func SkillsDir() string {
-	return filepath.Join(ClaudeDir(), "skills")
-}
-
-// AgentsDir returns the agents directory
-func AgentsDir() string {
-	return filepath.Join(ClaudeDir(), "agents")
-}
-
-// HistoryDir returns the history directory
-func HistoryDir() string {
-	return filepath.Join(ClaudeDir(), "history")
-}
-
-// GetHistoryFilePath returns a history file path with year-month organization
-func GetHistoryFilePath(subdir, filename string) string {
-	now := time.Now()
-	yearMonth := now.Format("2006-01")
-	return filepath.Join(HistoryDir(), subdir, yearMonth, filename)
-}
-
-// GetTimestamp returns current timestamp in YYYY-MM-DD-HHMMSS format
-func GetTimestamp() string {
-	return time.Now().Format("2006-01-02-150405")
-}
-
-// GetDate returns current date in YYYY-MM-DD format
-func GetDate() string {
-	return time.Now().Format("2006-01-02")
-}
-
-// GetYearMonth returns current year-month in YYYY-MM format
-func GetYearMonth() string {
-	return time.Now().Format("2006-01")
-}
-
-// ValidateClaudeStructure validates that the Claude directory exists
-func ValidateClaudeStructure() error {
-	claudeDir := ClaudeDir()
-	if _, err := os.Stat(claudeDir); os.IsNotExist(err) {
-		return fmt.Errorf("CLAUDE_DIR does not exist: %s\nExpected ~/.claude or set CLAUDE_DIR environment variable", claudeDir)
-	}
-	return nil
-}
-
-// EnsureHistoryDir creates the history directory structure if needed
-func EnsureHistoryDir(subdir string) error {
-	yearMonth := GetYearMonth()
-	dir := filepath.Join(HistoryDir(), subdir, yearMonth)
-	return os.MkdirAll(dir, 0755)
-}
tools/claude-hooks/default.nix
@@ -1,41 +0,0 @@
-{
-  buildGoModule,
-  lib,
-}:
-
-buildGoModule {
-  pname = "claude-hooks";
-  version = "0.1.0";
-  src = ./.;
-
-  vendorHash = "sha256-bdpAteulG3045jPdEpjcT4yGlnxLKDMlK7lk9WVRTKc=";
-
-  # Build all binaries
-  subPackages = [
-    "cmd/capture-tool-output"
-    "cmd/initialize-session"
-    "cmd/update-terminal-title"
-    "cmd/validate-docs"
-    "cmd/save-session"
-    "cmd/session-stats"
-    "cmd/validate-git-push"
-  ];
-
-  # Rename binaries to have consistent prefix
-  postInstall = ''
-    mv $out/bin/capture-tool-output $out/bin/claude-hooks-capture-tool-output
-    mv $out/bin/initialize-session $out/bin/claude-hooks-initialize-session
-    mv $out/bin/update-terminal-title $out/bin/claude-hooks-update-terminal-title
-    mv $out/bin/validate-docs $out/bin/claude-hooks-validate-docs
-    mv $out/bin/save-session $out/bin/claude-hooks-save-session
-    mv $out/bin/session-stats $out/bin/claude-hooks-session-stats
-    mv $out/bin/validate-git-push $out/bin/claude-hooks-validate-git-push
-  '';
-
-  meta = {
-    description = "Claude Code hooks for session management, tool output capture, and documentation validation";
-    license = lib.licenses.mit;
-    platforms = lib.platforms.unix;
-    mainProgram = "claude-hooks-capture-tool-output";
-  };
-}
tools/claude-hooks/go.mod
@@ -1,5 +0,0 @@
-module github.com/vdemeester/home/tools/claude-hooks
-
-go 1.23
-
-require github.com/bmatcuk/doublestar/v4 v4.7.1
tools/claude-hooks/go.sum
@@ -1,2 +0,0 @@
-github.com/bmatcuk/doublestar/v4 v4.7.1 h1:fdDeAqgT47acgwd9bd9HxJRDmc9UAmPpc+2m0CXv75Q=
-github.com/bmatcuk/doublestar/v4 v4.7.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
tools/claude-hooks/README.md
@@ -1,256 +0,0 @@
-# Claude Code Hooks (Go Implementation)
-
-Claude Code hooks implemented in Go for better performance, zero runtime dependencies, and native integration with the Nix ecosystem.
-
-Migrated from TypeScript/Bun implementation in `dots/.claude/hooks/`.
-
-## Hooks Included
-
-### 1. `claude-hooks-initialize-session`
-**Event**: SessionStart
-
-**What it does**:
-- Detects and skips subagent sessions
-- Sets terminal tab title to "Claude Ready"
-- Logs session start to `~/.config/claude/history/sessions/YYYY-MM/YYYY-MM-DD_session-log.txt`
-- Implements debouncing to prevent duplicate triggers (2 second window)
-
-### 2. `claude-hooks-capture-tool-output`
-**Event**: PostToolUse
-
-**What it does**:
-- Captures outputs from interesting tools (Bash, Edit, Write, Read, Task, NotebookEdit, Skill, SlashCommand)
-- Logs to JSONL files: `~/.config/claude/history/tool-outputs/YYYY-MM/YYYY-MM-DD_tool-outputs.jsonl`
-- Silent failure - doesn't disrupt workflow if logging fails
-- Automatically creates directory structure
-
-### 3. `claude-hooks-validate-docs`
-**Event**: Manual/Pre-commit
-
-**What it does**:
-- Scans all markdown files for internal links
-- Checks if linked files exist
-- Reports broken links with file:line numbers
-- Exit code 0 if valid, 1 if broken links found
-- Skips external URLs, anchors, and mailto links
-
-### 4. `claude-hooks-save-session`
-**Event**: SessionEnd
-
-**What it does**:
-- Prompts user to save session summary when ending a Claude Code session
-- Skips subagent sessions (silent)
-- Displays a message asking if session should be saved
-- Works with `/save-session` slash command for creating session entries
-- Saves to `~/.config/claude/history/sessions/YYYY-MM/YYYY-MM-DD-HHMMSS_SESSION_description.md`
-
-### 5. `claude-hooks-validate-git-push`
-**Event**: PreToolUse (via extensions)
-
-**What it does**:
-- Validates git commands to prevent dangerous operations
-- Blocks `git add -A`, `git add --all`, and `git add .` to prevent adding unintended files
-- Blocks git push commands without explicit refspecs (e.g., `git push origin main`)
-- Warns about pushes to protected branches (main/master)
-- Exit code 0 if command is safe, non-zero if command should be blocked
-
-## Architecture
-
-```
-tools/claude-hooks/
-├── cmd/
-│   ├── capture-tool-output/main.go   # PostToolUse hook
-│   ├── initialize-session/main.go    # SessionStart hook
-│   ├── save-session/main.go          # SessionEnd hook
-│   └── validate-docs/main.go         # Documentation validator
-├── internal/
-│   └── paths/paths.go                # Shared path utilities
-├── default.nix                       # Nix package definition
-├── go.mod                            # Go module definition
-├── go.sum                            # Go dependencies
-├── setup-hooks.sh                    # Configuration script
-└── README.md                         # This file
-```
-
-## Installation
-
-### Via Nix (Recommended)
-
-1. **Build the package**:
-   ```bash
-   nix build .#claude-hooks
-   ```
-
-2. **Install to your profile**:
-   ```bash
-   nix profile install .#claude-hooks
-   ```
-
-3. **Or add to home-manager** (in `home/common/dev/default.nix` or similar):
-   ```nix
-   home.packages = with pkgs; [
-     claude-hooks
-   ];
-   ```
-
-4. **Configure hooks** (run setup script):
-   ```bash
-   ./tools/claude-hooks/setup-hooks.sh
-   ```
-
-   Or manually update `~/.claude/settings.json`:
-   ```json
-   {
-     "hooks": {
-       "SessionStart": [
-         {
-           "hooks": [
-             {
-               "type": "command",
-               "command": "claude-hooks-initialize-session"
-             }
-           ]
-         }
-       ],
-       "PostToolUse": [
-         {
-           "hooks": [
-             {
-               "type": "command",
-               "command": "claude-hooks-capture-tool-output"
-             }
-           ]
-         }
-       ],
-       "SessionEnd": [
-         {
-           "hooks": [
-             {
-               "type": "command",
-               "command": "claude-hooks-save-session"
-             }
-           ]
-         }
-       ]
-     }
-   }
-   ```
-
-5. **Enable the session-manager plugin** (for `/save-session` command):
-   ```bash
-   # Symlink the plugin if not already linked
-   ln -s ~/src/home/dots/.claude/plugins/session-manager ~/.claude/plugins/session-manager
-   ```
-
-### Manual Build (without Nix)
-
-```bash
-cd tools/claude-hooks
-
-# Build all hooks
-go build -o bin/claude-hooks-capture-tool-output ./cmd/capture-tool-output
-go build -o bin/claude-hooks-initialize-session ./cmd/initialize-session
-go build -o bin/claude-hooks-save-session ./cmd/save-session
-go build -o bin/claude-hooks-validate-docs ./cmd/validate-docs
-
-# Copy to PATH
-sudo cp bin/* /usr/local/bin/
-
-# Then run setup script
-./setup-hooks.sh
-```
-
-## Development
-
-### Running hooks directly with `go run`
-
-```bash
-cd tools/claude-hooks
-
-# Test initialize-session
-go run ./cmd/initialize-session
-
-# Test capture-tool-output (requires JSON input)
-echo '{"tool_name":"Bash","tool_input":{},"tool_response":{},"conversation_id":"test"}' | \
-  go run ./cmd/capture-tool-output
-
-# Test validate-docs
-go run ./cmd/validate-docs
-```
-
-### Testing
-
-```bash
-# Run all tests
-go test ./...
-
-# Test with verbose output
-go test -v ./internal/paths
-
-# Test specific package
-go test ./cmd/validate-docs
-```
-
-### Updating vendorHash
-
-If you add/update Go dependencies:
-
-```bash
-cd tools/claude-hooks
-go mod tidy
-nix build .#claude-hooks 2>&1 | grep "got:" | awk '{print $2}'
-# Copy the hash to default.nix vendorHash field
-```
-
-## Advantages over TypeScript/Bun
-
-1. **Zero runtime dependencies** - Compiled to native binary, no Bun required
-2. **Faster execution** - Native code vs interpreted JavaScript
-3. **Easier distribution** - Single binary per hook
-4. **Cross-compilation** - Build for any architecture from any platform
-5. **Nix integration** - Proper package management and reproducible builds
-6. **Type safety** - Compile-time checks without runtime overhead
-7. **Standard library** - Excellent built-in support for JSON, file I/O, paths
-
-## Migration Notes
-
-The Go implementation is functionally equivalent to the TypeScript version with these improvements:
-
-- **Performance**: Faster startup and execution
-- **Reliability**: Compile-time type checking
-- **Portability**: Works on all systems where Go binaries run
-- **Integration**: Native Nix packaging for declarative installation
-
-The old TypeScript hooks in `dots/.claude/hooks/` can be removed after verifying the Go versions work correctly.
-
-## Troubleshooting
-
-**Hook not running:**
-- Check `~/.claude/settings.json` syntax
-- Verify binaries are in PATH: `which claude-hooks-initialize-session`
-- Check stderr output in terminal
-- Verify hooks are executable: `ls -la $(which claude-hooks-initialize-session)`
-
-**Permission errors:**
-- Ensure directories exist: `mkdir -p ~/.config/claude/history/{sessions,tool-outputs}`
-- Check write permissions on `~/.config/claude/history`
-
-**Debugging:**
-- Hooks write errors to stderr - check terminal output
-- Run hooks manually to test: `claude-hooks-initialize-session`
-- Check log files: `ls -la ~/.config/claude/history/`
-
-**Build errors:**
-- Run `go mod tidy` to update dependencies
-- Check Go version: `go version` (requires Go 1.23+)
-- Verify vendorHash in `default.nix` is correct
-
-## Environment Variables
-
-- `CLAUDE_DIR` - Override default `~/.config/claude` directory (optional)
-- `CLAUDE_PROJECT_DIR` - Set by Claude Code (used for subagent detection)
-- `CLAUDE_AGENT_TYPE` - Set by Claude Code for subagents (used for detection)
-
-## License
-
-Same as the rest of the home repository.
tools/claude-hooks/setup-hooks.sh
@@ -1,104 +0,0 @@
-#!/usr/bin/env bash
-#
-# setup-hooks.sh - Configure Claude Code hooks to use Nix-built binaries
-#
-# This script updates ~/.claude/settings.json to reference the correct
-# hook binaries from the Nix store (via PATH).
-#
-# Usage:
-#   ./setup-hooks.sh              # Update settings.json
-#   ./setup-hooks.sh --dry-run    # Show what would be changed
-
-set -euo pipefail
-
-SETTINGS_FILE="${HOME}/.claude/settings.json"
-SETTINGS_BACKUP="${HOME}/.claude/settings.json.backup"
-
-# Determine if this is a dry run
-DRY_RUN=false
-if [[ "${1:-}" == "--dry-run" ]]; then
-    DRY_RUN=true
-fi
-
-# Check if settings.json exists
-if [[ ! -f "$SETTINGS_FILE" ]]; then
-    echo "Error: $SETTINGS_FILE does not exist"
-    echo "Please create it first or run Claude Code to initialize it"
-    exit 1
-fi
-
-# Backup existing settings
-if [[ "$DRY_RUN" == false ]]; then
-    cp "$SETTINGS_FILE" "$SETTINGS_BACKUP"
-    echo "Backed up existing settings to: $SETTINGS_BACKUP"
-fi
-
-# Read current settings
-CURRENT_SETTINGS=$(cat "$SETTINGS_FILE")
-
-# Build new hooks configuration
-# Note: The binaries should be in PATH after installing via Nix
-NEW_HOOKS=$(cat <<'EOF'
-{
-  "hooks": [
-    {
-      "type": "command",
-      "command": "claude-hooks-initialize-session"
-    }
-  ]
-}
-EOF
-)
-
-POST_TOOL_HOOKS=$(cat <<'EOF'
-{
-  "hooks": [
-    {
-      "type": "command",
-      "command": "claude-hooks-capture-tool-output"
-    }
-  ]
-}
-EOF
-)
-
-SESSION_END_HOOKS=$(cat <<'EOF'
-{
-  "hooks": [
-    {
-      "type": "command",
-      "command": "claude-hooks-save-session"
-    }
-  ]
-}
-EOF
-)
-
-# Use jq to update the settings.json with proper JSON merging
-UPDATED_SETTINGS=$(echo "$CURRENT_SETTINGS" | jq --argjson sessionStart "$NEW_HOOKS" \
-    --argjson postTool "$POST_TOOL_HOOKS" \
-    --argjson sessionEnd "$SESSION_END_HOOKS" \
-    '.hooks.SessionStart = [$sessionStart] | .hooks.PostToolUse = [$postTool] | .hooks.SessionEnd = [$sessionEnd]')
-
-if [[ "$DRY_RUN" == true ]]; then
-    echo "=== DRY RUN - No changes made ==="
-    echo ""
-    echo "Would update $SETTINGS_FILE with:"
-    echo "$UPDATED_SETTINGS" | jq '.'
-    echo ""
-    echo "Run without --dry-run to apply these changes"
-else
-    echo "$UPDATED_SETTINGS" | jq '.' > "$SETTINGS_FILE"
-    echo "Successfully updated $SETTINGS_FILE"
-    echo ""
-    echo "Hook binaries configured:"
-    echo "  - SessionStart: claude-hooks-initialize-session"
-    echo "  - PostToolUse: claude-hooks-capture-tool-output"
-    echo "  - SessionEnd: claude-hooks-save-session"
-    echo ""
-    echo "Make sure claude-hooks is installed:"
-    echo "  nix profile install .#claude-hooks"
-    echo ""
-    echo "Or add to your home-manager configuration:"
-    echo "  home.packages = [ pkgs.claude-hooks ];"
-fi