package main import ( "encoding/json" "net/http" "time" ) // HookEvent represents the JSON payload sent by Claude Code hooks. type HookEvent struct { SessionID string `json:"session_id"` TranscriptPath string `json:"transcript_path,omitempty"` Cwd string `json:"cwd,omitempty"` HookEventName string `json:"hook_event_name"` NotificationType string `json:"notification_type,omitempty"` Message string `json:"message,omitempty"` Title string `json:"title,omitempty"` LastAssistantMsg string `json:"last_assistant_message,omitempty"` StopHookActive bool `json:"stop_hook_active,omitempty"` ToolName string `json:"tool_name,omitempty"` } // handleHook is the HTTP handler for POST /hook. // Validates method, limits body size, decodes JSON, and dispatches to processHookEvent. func (d *Daemon) handleHook(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } r.Body = http.MaxBytesReader(w, r.Body, 64*1024) var event HookEvent if err := json.NewDecoder(r.Body).Decode(&event); err != nil { http.Error(w, "bad request", http.StatusBadRequest) return } d.processHookEvent(event) w.WriteHeader(http.StatusOK) } // processHookEvent maps a Claude Code hook event to a registry update. // Ignores empty session IDs and unknown event types. func (d *Daemon) processHookEvent(event HookEvent) { if event.SessionID == "" { return } var state, waitType string switch event.HookEventName { case "Notification": state = "Needs Input" switch event.NotificationType { case "permission_prompt": waitType = "permission" case "idle_prompt": waitType = "idle" default: waitType = "question" } case "Stop": state = "Needs Input" waitType = "question" case "PostToolUse", "PreToolUse": state = "Working" waitType = "" default: return } d.registry.UpdateFromHook(event.SessionID, state, waitType, event.Cwd) } // UpdateFromHook updates a session from a hook event. // Creates the entry if it doesn't exist yet (hook can arrive before first poll). func (r *SessionRegistry) UpdateFromHook(sessionID, state, waitType, cwd string) { r.mu.Lock() defer r.mu.Unlock() existing, ok := r.sessions[sessionID] if !ok { existing = &TrackedSession{} r.sessions[sessionID] = existing } prevState := existing.PrevState existing.Info.SessionID = sessionID existing.Info.State = state existing.Info.WaitType = waitType if cwd != "" { existing.Info.Cwd = cwd } // WaitingSince transition isWaiting := state == "Needs Input" wasWaiting := prevState == "Needs Input" if isWaiting && !wasWaiting { now := time.Now() existing.Info.WaitingSince = &now } else if !isWaiting { existing.Info.WaitingSince = nil } existing.PrevState = state }