flake-update-20260201
1package main
2
3import (
4 "fmt"
5 "os"
6 "path/filepath"
7 "strconv"
8 "strings"
9 "time"
10
11 "github.com/vdemeester/home/tools/claude-hooks/internal/paths"
12)
13
14const (
15 debounceDuration = 2 * time.Second
16)
17
18func getLockfile() string {
19 return filepath.Join(os.TempDir(), "claude-session-start.lock")
20}
21
22// shouldDebounce checks if we're within the debounce window
23func shouldDebounce() bool {
24 lockfile := getLockfile()
25
26 data, err := os.ReadFile(lockfile)
27 if err == nil {
28 lockTime, err := strconv.ParseInt(strings.TrimSpace(string(data)), 10, 64)
29 if err == nil {
30 now := time.Now().UnixMilli()
31 if now-lockTime < debounceDuration.Milliseconds() {
32 return true
33 }
34 }
35 }
36
37 // Update lockfile with current timestamp
38 now := time.Now().UnixMilli()
39 if err := os.WriteFile(lockfile, []byte(fmt.Sprintf("%d", now)), 0644); err != nil {
40 // Ignore write errors
41 }
42
43 return false
44}
45
46// isSubagentSession checks if this is a subagent session
47func isSubagentSession() bool {
48 claudeProjectDir := os.Getenv("CLAUDE_PROJECT_DIR")
49 if strings.Contains(claudeProjectDir, "/.claude/agents/") {
50 return true
51 }
52 if os.Getenv("CLAUDE_AGENT_TYPE") != "" {
53 return true
54 }
55 return false
56}
57
58// setTerminalTitle sets the terminal tab title using ANSI escape codes
59func setTerminalTitle(title string) {
60 fmt.Fprintf(os.Stderr, "\x1b]0;%s\x07", title)
61 fmt.Fprintf(os.Stderr, "\x1b]2;%s\x07", title)
62 fmt.Fprintf(os.Stderr, "\x1b]30;%s\x07", title)
63}
64
65// logSessionStart logs the session start to history
66func logSessionStart() error {
67 timestamp := paths.GetTimestamp()
68 yearMonth := timestamp[:7] // YYYY-MM
69
70 logDir := filepath.Join(paths.HistoryDir(), "sessions", yearMonth)
71 if err := os.MkdirAll(logDir, 0755); err != nil {
72 return err
73 }
74
75 logEntry := fmt.Sprintf("%s - Session started\n", time.Now().Format(time.RFC3339))
76 logFile := filepath.Join(logDir, fmt.Sprintf("%s_session-log.txt", timestamp[:10]))
77
78 f, err := os.OpenFile(logFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
79 if err != nil {
80 return err
81 }
82 defer f.Close()
83
84 _, err = f.WriteString(logEntry)
85 return err
86}
87
88// loadCoreSkill outputs the CORE skill content so Claude receives it at session start
89func loadCoreSkill() error {
90 homeDir, err := os.UserHomeDir()
91 if err != nil {
92 return err
93 }
94
95 coreSkillPath := filepath.Join(homeDir, ".config/claude/skills/CORE/SKILL.md")
96 content, err := os.ReadFile(coreSkillPath)
97 if err != nil {
98 return err
99 }
100
101 // Output to stdout so Claude receives it
102 fmt.Println("\n<!-- CORE Skill Auto-Loaded at Session Start -->")
103 fmt.Println(string(content))
104 fmt.Println("<!-- End CORE Skill -->")
105
106 return nil
107}
108
109func main() {
110 // Check if this is a subagent session
111 if isSubagentSession() {
112 fmt.Fprintln(os.Stderr, "🤖 Subagent session detected - skipping session initialization")
113 os.Exit(0)
114 }
115
116 // Check debounce to prevent duplicate notifications
117 if shouldDebounce() {
118 fmt.Fprintln(os.Stderr, "🔇 Debouncing duplicate SessionStart event")
119 os.Exit(0)
120 }
121
122 // Set initial tab title
123 tabTitle := "Claude Ready"
124 setTerminalTitle(tabTitle)
125 fmt.Fprintf(os.Stderr, "📍 Session initialized: \"%s\"\n", tabTitle)
126
127 // Load CORE skill at session start
128 if err := loadCoreSkill(); err != nil {
129 // Warn but don't break session start
130 fmt.Fprintf(os.Stderr, "[initialize-session] Warning: Could not load CORE skill: %v\n", err)
131 }
132
133 // Ring terminal bell to notify user (works with kitty bell_on_tab)
134 fmt.Fprint(os.Stderr, "\a")
135
136 // Log session start to history (silent failure)
137 if err := logSessionStart(); err != nil {
138 // Don't break session start for logging issues
139 fmt.Fprintf(os.Stderr, "[initialize-session] Warning: Could not log session start: %v\n", err)
140 }
141
142 os.Exit(0)
143}