mirror of
https://github.com/imjasonh/cnotes
synced 2026-07-22 15:50:41 +00:00
Scaffold all 7 hook events and eliminate type casting
- Add missing hook handlers: Stop, SubagentStop, PreCompact
- Replace all string type casting with proper JSON unmarshaling
- Change ToolUseRequest.Parameters from map[string]interface{} to json.RawMessage
- Update helper methods to use json.Unmarshal directly to typed structs
- Replace speech notifications with simple logging notifications
- Update documentation to reflect all hook events and remove speech references
- Add comprehensive CLAUDE.md with architecture overview and development guidance
All hooks now use type-safe JSON handling without .(string) assertions.
Complete coverage of Claude Code events: PreToolUse, PostToolUse,
UserPromptSubmit, Notification, Stop, SubagentStop, PreCompact.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
c61b579243
commit
45ef980233
10 changed files with 351 additions and 108 deletions
106
CLAUDE.md
Normal file
106
CLAUDE.md
Normal file
|
|
@ -0,0 +1,106 @@
|
||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## Build and Run Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build the binary
|
||||||
|
go build -o hooks
|
||||||
|
|
||||||
|
# Install hooks to Claude settings (project-level by default)
|
||||||
|
./hooks install
|
||||||
|
|
||||||
|
# Install to different scopes
|
||||||
|
./hooks install --global # ~/.claude/settings.json
|
||||||
|
./hooks install --local # ./.claude/settings.local.json
|
||||||
|
|
||||||
|
# Uninstall hooks
|
||||||
|
./hooks install --uninstall
|
||||||
|
|
||||||
|
# Test hook execution manually (for debugging)
|
||||||
|
echo '{"hook_event_name":"PreToolUse","tool_name":"Bash","tool_input":"{\"command\":\"ls\"}"}' | ./hooks run
|
||||||
|
|
||||||
|
# Run with debug logging
|
||||||
|
./hooks run --debug
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architecture Overview
|
||||||
|
|
||||||
|
This is a Claude Code hooks system that intercepts and processes all tool usage events. The architecture follows a registry pattern where individual hook handlers register themselves for specific events and tools.
|
||||||
|
|
||||||
|
### Core Components
|
||||||
|
|
||||||
|
**`internal/hooks/`** - Core hook engine
|
||||||
|
- `types.go` - Defines all hook input/output types and helper methods. Uses `json.RawMessage` for parameters to avoid type casting
|
||||||
|
- `registry.go` - Central registry for mapping events+tools to handler functions
|
||||||
|
- `runner.go` - Main execution engine that processes hook chains and handles JSON I/O
|
||||||
|
|
||||||
|
**`internal/handlers/`** - Hook implementations
|
||||||
|
- Each file implements handlers for specific events (pretooluse.go, posttooluse.go, etc.)
|
||||||
|
- Handlers auto-register in `init()` functions when package is imported
|
||||||
|
- All 7 Claude Code events are implemented: PreToolUse, PostToolUse, UserPromptSubmit, Notification, Stop, SubagentStop, PreCompact
|
||||||
|
|
||||||
|
**`internal/config/`** - Claude settings management
|
||||||
|
- `settings.go` - Handles reading/writing Claude's settings.json files with support for global, local, and project-level configurations
|
||||||
|
|
||||||
|
**`cmd/`** - CLI interface using Cobra
|
||||||
|
- `run.go` - Handles hook execution (called by Claude Code)
|
||||||
|
- `install.go` - Manages hook installation/uninstallation
|
||||||
|
|
||||||
|
### Type Safety and JSON Handling
|
||||||
|
|
||||||
|
The codebase avoids string type casting entirely by:
|
||||||
|
1. Using `json.RawMessage` for all parameter storage
|
||||||
|
2. Providing typed structs (`BashToolInput`, `FileToolInput`) for tool-specific data
|
||||||
|
3. Using `input.GetBashInput()`, `input.GetFileInput()` helper methods that unmarshal JSON directly to typed structs
|
||||||
|
4. Never using `.(string)` type assertions anywhere in the codebase
|
||||||
|
|
||||||
|
### Hook Registration Pattern
|
||||||
|
|
||||||
|
```go
|
||||||
|
func init() {
|
||||||
|
hooks.RegisterHook(hooks.EventPreToolUse, "Bash", ValidateBashCommand)
|
||||||
|
hooks.RegisterHook(hooks.EventPreToolUse, "Write|Edit|MultiEdit", PreventSensitiveFileEdits)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Matchers support:
|
||||||
|
- Exact tool names: `"Bash"`
|
||||||
|
- Regex patterns: `"Write|Edit|MultiEdit"`
|
||||||
|
- Wildcard: `"*"` for all tools
|
||||||
|
|
||||||
|
### Hook Function Signature
|
||||||
|
|
||||||
|
All hook functions follow this signature:
|
||||||
|
```go
|
||||||
|
func HookName(ctx context.Context, input hooks.HookInput) (hooks.HookOutput, error)
|
||||||
|
```
|
||||||
|
|
||||||
|
Hook outputs can:
|
||||||
|
- Block execution: `Decision: "block"`
|
||||||
|
- Allow with modifications: `Decision: "approve"` + `ModifiedParameters`
|
||||||
|
- Add context to responses: `AdditionalContext`
|
||||||
|
- Modify user prompts: `ModifiedUserPrompt`
|
||||||
|
|
||||||
|
### Settings File Structure
|
||||||
|
|
||||||
|
The system uses Claude's native hook configuration format with proper event name mapping:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"hooks": {
|
||||||
|
"PreToolUse": [{"matcher": ".*", "hooks": [{"type": "command", "command": "/path/to/hooks run"}]}],
|
||||||
|
"PostToolUse": [{"matcher": ".*", "hooks": [{"type": "command", "command": "/path/to/hooks run"}]}]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Key Design Principles
|
||||||
|
|
||||||
|
- **No Type Casting**: All JSON handling uses proper unmarshaling to typed structs
|
||||||
|
- **Auto-Registration**: Hook handlers register themselves via `init()` functions
|
||||||
|
- **Comprehensive Coverage**: All 7 Claude Code events are implemented with placeholder functionality
|
||||||
|
- **Project-First**: Defaults to project-level `.claude/settings.json` installation
|
||||||
|
- **Extensible**: Easy to add new hook handlers by creating new files in `internal/handlers/`
|
||||||
|
|
||||||
|
When adding new hooks, create a new file in `internal/handlers/`, implement the hook function, and register it in an `init()` function. The handler will be automatically loaded when the package is imported.
|
||||||
39
README.md
39
README.md
|
|
@ -43,14 +43,18 @@ func init() {
|
||||||
|
|
||||||
```go
|
```go
|
||||||
func BlockRmRf(ctx context.Context, input hooks.HookInput) (hooks.HookOutput, error) {
|
func BlockRmRf(ctx context.Context, input hooks.HookInput) (hooks.HookOutput, error) {
|
||||||
cmd := input.ToolUseRequest.Parameters["command"].(string)
|
bashInput, err := input.GetBashInput()
|
||||||
if strings.Contains(cmd, "rm -rf /") {
|
if err != nil {
|
||||||
|
return hooks.HookOutput{Decision: "approve"}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.Contains(bashInput.Command, "rm -rf /") {
|
||||||
return hooks.HookOutput{
|
return hooks.HookOutput{
|
||||||
Decision: "block",
|
Decision: "block",
|
||||||
Reason: "Cannot execute rm -rf on root directory",
|
Reason: "Cannot execute rm -rf on root directory",
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
return hooks.HookOutput{Decision: "continue"}, nil
|
return hooks.HookOutput{Decision: "approve"}, nil
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -70,25 +74,29 @@ func AddGitInfo(ctx context.Context, input hooks.HookInput) (hooks.HookOutput, e
|
||||||
|
|
||||||
```go
|
```go
|
||||||
func ForceNonInteractiveSudo(ctx context.Context, input hooks.HookInput) (hooks.HookOutput, error) {
|
func ForceNonInteractiveSudo(ctx context.Context, input hooks.HookInput) (hooks.HookOutput, error) {
|
||||||
cmd := input.ToolUseRequest.Parameters["command"].(string)
|
bashInput, err := input.GetBashInput()
|
||||||
if strings.Contains(cmd, "sudo") && !strings.Contains(cmd, "sudo -n") {
|
if err != nil {
|
||||||
|
return hooks.HookOutput{Decision: "approve"}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.Contains(bashInput.Command, "sudo") && !strings.Contains(bashInput.Command, "sudo -n") {
|
||||||
return hooks.HookOutput{
|
return hooks.HookOutput{
|
||||||
Decision: "continue",
|
Decision: "approve",
|
||||||
ModifiedParameters: map[string]interface{}{
|
ModifiedParameters: map[string]interface{}{
|
||||||
"command": strings.ReplaceAll(cmd, "sudo", "sudo -n"),
|
"command": strings.ReplaceAll(bashInput.Command, "sudo", "sudo -n"),
|
||||||
},
|
},
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
return hooks.HookOutput{Decision: "continue"}, nil
|
return hooks.HookOutput{Decision: "approve"}, nil
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Built-in Hooks
|
## Built-in Hooks
|
||||||
|
|
||||||
This package includes several example hooks:
|
This package includes comprehensive hook implementations for all Claude Code events:
|
||||||
|
|
||||||
### Pre Tool Use
|
### Pre Tool Use
|
||||||
- **Bash Command Validator**: Blocks dangerous commands like `rm -rf /`
|
- **Bash Command Validator**: Blocks dangerous commands using regex patterns
|
||||||
- **Sensitive File Protection**: Prevents editing `.env`, `.aws/credentials`, etc.
|
- **Sensitive File Protection**: Prevents editing `.env`, `.aws/credentials`, etc.
|
||||||
|
|
||||||
### Post Tool Use
|
### Post Tool Use
|
||||||
|
|
@ -99,7 +107,16 @@ This package includes several example hooks:
|
||||||
- **Project Context**: Adds git branch and project type information
|
- **Project Context**: Adds git branch and project type information
|
||||||
|
|
||||||
### Notification
|
### Notification
|
||||||
- **Speech Notifications**: Uses macOS 'say' command to speak notifications aloud
|
- **System Notifications**: Logs notification events for debugging and monitoring
|
||||||
|
|
||||||
|
### Stop
|
||||||
|
- **Session Completion**: Logs when main Claude agent finishes responding
|
||||||
|
|
||||||
|
### SubagentStop
|
||||||
|
- **Subagent Completion**: Logs when Task tool subagents finish responding
|
||||||
|
|
||||||
|
### PreCompact
|
||||||
|
- **Context Compaction**: Handles before context window compaction (manual/auto)
|
||||||
|
|
||||||
## Testing Hooks
|
## Testing Hooks
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -87,7 +87,10 @@ func runInstall(cmd *cobra.Command, args []string) error {
|
||||||
fmt.Println(" • pre_tool_use: Validates bash commands and prevents sensitive file edits")
|
fmt.Println(" • pre_tool_use: Validates bash commands and prevents sensitive file edits")
|
||||||
fmt.Println(" • post_tool_use: Logs tool usage and runs goimports on modified Go files")
|
fmt.Println(" • post_tool_use: Logs tool usage and runs goimports on modified Go files")
|
||||||
fmt.Println(" • user_prompt_submit: Adds project context to prompts")
|
fmt.Println(" • user_prompt_submit: Adds project context to prompts")
|
||||||
fmt.Println(" • notification: Speaks notifications aloud (macOS)")
|
fmt.Println(" • notification: Logs notification events")
|
||||||
|
fmt.Println(" • stop: Logs session completion")
|
||||||
|
fmt.Println(" • subagent_stop: Logs subagent completion")
|
||||||
|
fmt.Println(" • pre_compact: Handles context compaction events")
|
||||||
fmt.Println("\nTo test: echo '{\"event\":\"pre_tool_use\",\"tool\":\"Bash\",\"tool_use_request\":{\"tool\":\"Bash\",\"parameters\":{\"command\":\"ls\"}}}' | hooks run")
|
fmt.Println("\nTo test: echo '{\"event\":\"pre_tool_use\",\"tool\":\"Bash\",\"tool_use_request\":{\"tool\":\"Bash\",\"parameters\":{\"command\":\"ls\"}}}' | hooks run")
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,16 @@
|
||||||
|
// Package handlers provides hook implementations for all Claude Code events.
|
||||||
|
//
|
||||||
|
// The following hook events are implemented:
|
||||||
|
// - PreToolUse: Command validation and sensitive file protection (pretooluse.go)
|
||||||
|
// - PostToolUse: Tool usage logging and goimports automation (posttooluse.go, goimports.go)
|
||||||
|
// - UserPromptSubmit: Project context injection (userprompt.go)
|
||||||
|
// - Notification: System notification logging (notification.go)
|
||||||
|
// - Stop: Session completion logging (stop.go)
|
||||||
|
// - SubagentStop: Subagent completion logging (subagentstop.go)
|
||||||
|
// - PreCompact: Context compaction handling (precompact.go)
|
||||||
|
//
|
||||||
|
// Each handler file registers its hooks in init() functions that are automatically
|
||||||
|
// called when the package is imported.
|
||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|
@ -5,10 +18,13 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
// PreToolUse hooks - validation and security
|
||||||
hooks.RegisterHook(hooks.EventPreToolUse, "Bash", ValidateBashCommand)
|
hooks.RegisterHook(hooks.EventPreToolUse, "Bash", ValidateBashCommand)
|
||||||
hooks.RegisterHook(hooks.EventPreToolUse, "Write|Edit|MultiEdit", PreventSensitiveFileEdits)
|
hooks.RegisterHook(hooks.EventPreToolUse, "Write|Edit|MultiEdit", PreventSensitiveFileEdits)
|
||||||
|
|
||||||
|
// PostToolUse hooks - logging and automation
|
||||||
hooks.RegisterHook(hooks.EventPostToolUse, "*", LogToolUsage)
|
hooks.RegisterHook(hooks.EventPostToolUse, "*", LogToolUsage)
|
||||||
|
|
||||||
|
// UserPromptSubmit hooks - context enhancement
|
||||||
hooks.RegisterHook(hooks.EventUserPromptSubmit, "*", AddProjectContext)
|
hooks.RegisterHook(hooks.EventUserPromptSubmit, "*", AddProjectContext)
|
||||||
}
|
}
|
||||||
|
|
@ -2,78 +2,54 @@ package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"os/exec"
|
"time"
|
||||||
"runtime"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/imjasonh/hooks/internal/hooks"
|
"github.com/imjasonh/hooks/internal/hooks"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
hooks.RegisterHook(hooks.EventNotification, "*", SpeakNotification)
|
hooks.RegisterHook(hooks.EventNotification, "*", LogNotification)
|
||||||
}
|
}
|
||||||
|
|
||||||
func SpeakNotification(ctx context.Context, input hooks.HookInput) (hooks.HookOutput, error) {
|
// LogNotification logs notification events for debugging and monitoring.
|
||||||
// Only run on macOS
|
// This hook is triggered when Claude needs permission to use a tool or when
|
||||||
if runtime.GOOS != "darwin" {
|
// prompt input has been idle for 60+ seconds.
|
||||||
return hooks.HookOutput{Decision: "approve"}, nil
|
//
|
||||||
}
|
// Common use cases:
|
||||||
|
// - Debug notification flow
|
||||||
// Build the notification content for speech
|
// - Monitor permission requests
|
||||||
|
// - Track user idle states
|
||||||
|
// - Audit notification patterns
|
||||||
|
func LogNotification(ctx context.Context, input hooks.HookInput) (hooks.HookOutput, error) {
|
||||||
var message string
|
var message string
|
||||||
|
var notificationType string
|
||||||
|
|
||||||
// Check if we have the direct message format (newer)
|
// Check if we have the direct message format (newer)
|
||||||
if input.Message != "" {
|
if input.Message != "" {
|
||||||
message = input.Message
|
message = input.Message
|
||||||
|
notificationType = "direct"
|
||||||
} else if input.Notification.Permission != "" {
|
} else if input.Notification.Permission != "" {
|
||||||
// Permission request (older format)
|
// Permission request (older format)
|
||||||
message = fmt.Sprintf("Requesting permission to %s", input.Notification.Message)
|
message = input.Notification.Message
|
||||||
|
notificationType = "permission"
|
||||||
} else if input.Notification.Message != "" {
|
} else if input.Notification.Message != "" {
|
||||||
// Regular notification (older format)
|
// Regular notification (older format)
|
||||||
message = input.Notification.Message
|
message = input.Notification.Message
|
||||||
|
notificationType = "message"
|
||||||
} else {
|
} else {
|
||||||
// No valid notification content
|
// No valid notification content
|
||||||
|
slog.Debug("notification hook triggered with no message content", "session_id", input.SessionID)
|
||||||
return hooks.HookOutput{Decision: "approve"}, nil
|
return hooks.HookOutput{Decision: "approve"}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Speak the notification using macOS say command
|
slog.Info("notification received",
|
||||||
if _, err := exec.LookPath("say"); err == nil {
|
"type", notificationType,
|
||||||
// Build more informative spoken message
|
"message", message,
|
||||||
var spokenMessage string
|
"tool", input.Notification.Tool,
|
||||||
if input.Notification.Permission != "" {
|
"permission", input.Notification.Permission != "",
|
||||||
spokenMessage = fmt.Sprintf("Claude is requesting permission for %s: %s", input.Notification.Tool, message)
|
"session_id", input.SessionID,
|
||||||
} else if input.Notification.Tool != "" {
|
"timestamp", time.Now().Unix())
|
||||||
spokenMessage = fmt.Sprintf("Claude notification from %s: %s", input.Notification.Tool, message)
|
|
||||||
} else {
|
|
||||||
spokenMessage = fmt.Sprintf("Claude notification: %s", message)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sanitize for speech
|
|
||||||
spokenMessage = strings.ReplaceAll(spokenMessage, "\n", " ")
|
|
||||||
spokenMessage = strings.ReplaceAll(spokenMessage, "\"", "'")
|
|
||||||
|
|
||||||
// Truncate if too long
|
|
||||||
if len(spokenMessage) > 200 {
|
|
||||||
spokenMessage = spokenMessage[:197] + "..."
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd := exec.CommandContext(ctx, "say", "-v", "Samantha", spokenMessage)
|
|
||||||
if err := cmd.Start(); err != nil {
|
|
||||||
slog.Debug("failed to start say command", "error", err)
|
|
||||||
} else {
|
|
||||||
// Don't wait for completion to avoid blocking
|
|
||||||
go func() {
|
|
||||||
if err := cmd.Wait(); err != nil {
|
|
||||||
slog.Debug("say command failed", "error", err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
slog.Info("spoke notification",
|
|
||||||
"tool", input.Notification.Tool,
|
|
||||||
"permission", input.Notification.Permission != "")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return hooks.HookOutput{Decision: "approve"}, nil
|
return hooks.HookOutput{Decision: "approve"}, nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,31 +9,29 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func LogToolUsage(ctx context.Context, input hooks.HookInput) (hooks.HookOutput, error) {
|
func LogToolUsage(ctx context.Context, input hooks.HookInput) (hooks.HookOutput, error) {
|
||||||
params := input.ToolUseRequest.Parameters
|
|
||||||
|
|
||||||
switch input.Tool {
|
switch input.Tool {
|
||||||
case "Bash":
|
case "Bash":
|
||||||
if cmd, ok := params["command"].(string); ok {
|
if bashInput, err := input.GetBashInput(); err == nil {
|
||||||
slog.Info("bash command executed",
|
slog.Info("bash command executed",
|
||||||
"command", cmd,
|
"command", bashInput.Command,
|
||||||
"session_id", input.SessionID,
|
"session_id", input.SessionID,
|
||||||
"cwd", input.CWD,
|
"cwd", input.CWD,
|
||||||
"timestamp", time.Now().Unix())
|
"timestamp", time.Now().Unix())
|
||||||
}
|
}
|
||||||
case "Write", "Edit", "MultiEdit":
|
case "Write", "Edit", "MultiEdit", "Read":
|
||||||
if path, ok := params["file_path"].(string); ok {
|
if fileInput, err := input.GetFileInput(); err == nil {
|
||||||
slog.Info("file modified",
|
if input.Tool == "Read" {
|
||||||
"tool", input.Tool,
|
slog.Info("file read",
|
||||||
"file", path,
|
"file", fileInput.FilePath,
|
||||||
"session_id", input.SessionID,
|
"session_id", input.SessionID,
|
||||||
"timestamp", time.Now().Unix())
|
"timestamp", time.Now().Unix())
|
||||||
}
|
} else {
|
||||||
case "Read":
|
slog.Info("file modified",
|
||||||
if path, ok := params["file_path"].(string); ok {
|
"tool", input.Tool,
|
||||||
slog.Info("file read",
|
"file", fileInput.FilePath,
|
||||||
"file", path,
|
"session_id", input.SessionID,
|
||||||
"session_id", input.SessionID,
|
"timestamp", time.Now().Unix())
|
||||||
"timestamp", time.Now().Unix())
|
}
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
slog.Info("tool used",
|
slog.Info("tool used",
|
||||||
|
|
|
||||||
69
internal/handlers/precompact.go
Normal file
69
internal/handlers/precompact.go
Normal file
|
|
@ -0,0 +1,69 @@
|
||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/imjasonh/hooks/internal/hooks"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
hooks.RegisterHook(hooks.EventPreCompact, "*", HandlePreCompact)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandlePreCompact is triggered before Claude Code runs a compact operation to
|
||||||
|
// reduce context window size. This can happen manually via /compact command
|
||||||
|
// or automatically when the context window fills up.
|
||||||
|
//
|
||||||
|
// The matcher indicates the compaction trigger:
|
||||||
|
// - "manual": User ran /compact command
|
||||||
|
// - "auto": Automatic compaction due to context window limit
|
||||||
|
//
|
||||||
|
// Common use cases:
|
||||||
|
// - Save conversation state before compaction
|
||||||
|
// - Warn user about potential data loss
|
||||||
|
// - Export important context to external files
|
||||||
|
// - Create conversation backups
|
||||||
|
// - Log compaction events for analysis
|
||||||
|
func HandlePreCompact(ctx context.Context, input hooks.HookInput) (hooks.HookOutput, error) {
|
||||||
|
// Determine compaction type from the tool name/matcher
|
||||||
|
compactionType := "unknown"
|
||||||
|
if input.Tool == "manual" || input.ToolName == "manual" {
|
||||||
|
compactionType = "manual"
|
||||||
|
} else if input.Tool == "auto" || input.ToolName == "auto" {
|
||||||
|
compactionType = "auto"
|
||||||
|
}
|
||||||
|
|
||||||
|
slog.Info("pre-compaction hook triggered",
|
||||||
|
"session_id", input.SessionID,
|
||||||
|
"compaction_type", compactionType,
|
||||||
|
"cwd", input.CWD,
|
||||||
|
"transcript_path", input.TranscriptPath,
|
||||||
|
"timestamp", time.Now().Unix())
|
||||||
|
|
||||||
|
// Add context warning for the user
|
||||||
|
var warningContext string
|
||||||
|
if compactionType == "manual" {
|
||||||
|
warningContext = "Manual compaction requested - some conversation history will be summarized"
|
||||||
|
} else if compactionType == "auto" {
|
||||||
|
warningContext = "Automatic compaction triggered - context window limit reached"
|
||||||
|
} else {
|
||||||
|
warningContext = "Context compaction about to occur"
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Future enhancements could include:
|
||||||
|
// - Export full conversation before compaction
|
||||||
|
// - Save important code snippets or commands
|
||||||
|
// - Create conversation timeline backup
|
||||||
|
// - Allow user to specify what to preserve
|
||||||
|
// - Integration with external note-taking systems
|
||||||
|
// - Automatic git commit of current work
|
||||||
|
// - Context importance scoring and preservation
|
||||||
|
|
||||||
|
return hooks.HookOutput{
|
||||||
|
Decision: "approve",
|
||||||
|
AdditionalContext: fmt.Sprintf("🔄 %s", warningContext),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
41
internal/handlers/stop.go
Normal file
41
internal/handlers/stop.go
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log/slog"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/imjasonh/hooks/internal/hooks"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
hooks.RegisterHook(hooks.EventStop, "*", LogSessionCompletion)
|
||||||
|
}
|
||||||
|
|
||||||
|
// LogSessionCompletion is triggered when the main Claude Code agent finishes responding.
|
||||||
|
// This hook runs after Claude has completed its response but does not run if the session
|
||||||
|
// was interrupted by the user.
|
||||||
|
//
|
||||||
|
// Common use cases:
|
||||||
|
// - Session cleanup and logging
|
||||||
|
// - Performance metrics collection
|
||||||
|
// - Workflow completion notifications
|
||||||
|
// - Resource cleanup
|
||||||
|
// - Export session summaries
|
||||||
|
func LogSessionCompletion(ctx context.Context, input hooks.HookInput) (hooks.HookOutput, error) {
|
||||||
|
slog.Info("Claude session completed",
|
||||||
|
"session_id", input.SessionID,
|
||||||
|
"cwd", input.CWD,
|
||||||
|
"transcript_path", input.TranscriptPath,
|
||||||
|
"timestamp", time.Now().Unix())
|
||||||
|
|
||||||
|
// TODO: Future enhancements could include:
|
||||||
|
// - Calculate session duration
|
||||||
|
// - Count tools used in session
|
||||||
|
// - Export conversation summary
|
||||||
|
// - Send completion notifications
|
||||||
|
// - Clean up temporary files
|
||||||
|
// - Archive transcript
|
||||||
|
|
||||||
|
return hooks.HookOutput{Decision: "approve"}, nil
|
||||||
|
}
|
||||||
42
internal/handlers/subagentstop.go
Normal file
42
internal/handlers/subagentstop.go
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log/slog"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/imjasonh/hooks/internal/hooks"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
hooks.RegisterHook(hooks.EventSubagentStop, "*", LogSubagentCompletion)
|
||||||
|
}
|
||||||
|
|
||||||
|
// LogSubagentCompletion is triggered when a Claude Code subagent (Task tool call)
|
||||||
|
// finishes responding. Subagents are created when Claude uses the Task tool to
|
||||||
|
// delegate work to another Claude instance.
|
||||||
|
//
|
||||||
|
// Common use cases:
|
||||||
|
// - Track nested workflow completion
|
||||||
|
// - Monitor subagent performance
|
||||||
|
// - Resource cleanup after Task operations
|
||||||
|
// - Debug complex multi-agent workflows
|
||||||
|
// - Collect metrics on task delegation
|
||||||
|
func LogSubagentCompletion(ctx context.Context, input hooks.HookInput) (hooks.HookOutput, error) {
|
||||||
|
slog.Info("Claude subagent completed",
|
||||||
|
"session_id", input.SessionID,
|
||||||
|
"cwd", input.CWD,
|
||||||
|
"transcript_path", input.TranscriptPath,
|
||||||
|
"timestamp", time.Now().Unix())
|
||||||
|
|
||||||
|
// TODO: Future enhancements could include:
|
||||||
|
// - Track subagent hierarchy depth
|
||||||
|
// - Measure subagent execution time
|
||||||
|
// - Collect subagent success/failure rates
|
||||||
|
// - Clean up subagent-specific resources
|
||||||
|
// - Aggregate results from multiple subagents
|
||||||
|
// - Send notifications for long-running subagents
|
||||||
|
// - Export subagent conversation logs
|
||||||
|
|
||||||
|
return hooks.HookOutput{Decision: "approve"}, nil
|
||||||
|
}
|
||||||
|
|
@ -5,38 +5,13 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/imjasonh/hooks/internal/hooks"
|
"github.com/imjasonh/hooks/internal/hooks"
|
||||||
)
|
)
|
||||||
|
|
||||||
func AddProjectContext(ctx context.Context, input hooks.HookInput) (hooks.HookOutput, error) {
|
func AddProjectContext(ctx context.Context, input hooks.HookInput) (hooks.HookOutput, error) {
|
||||||
// Speak notification for hook event on macOS
|
|
||||||
if runtime.GOOS == "darwin" {
|
|
||||||
if _, err := exec.LookPath("say"); err == nil {
|
|
||||||
// Extract first few words from prompt for context
|
|
||||||
promptPreview := input.Prompt
|
|
||||||
if len(promptPreview) > 50 {
|
|
||||||
promptPreview = promptPreview[:47] + "..."
|
|
||||||
}
|
|
||||||
// Remove newlines for speech
|
|
||||||
promptPreview = strings.ReplaceAll(promptPreview, "\n", " ")
|
|
||||||
|
|
||||||
spokenMessage := fmt.Sprintf("User prompt received: %s", promptPreview)
|
|
||||||
cmd := exec.CommandContext(ctx, "say", "-v", "Samantha", spokenMessage)
|
|
||||||
if err := cmd.Start(); err == nil {
|
|
||||||
go func() {
|
|
||||||
if err := cmd.Wait(); err != nil {
|
|
||||||
slog.Debug("say command failed", "error", err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var contexts []string
|
var contexts []string
|
||||||
|
|
||||||
if gitRoot := findGitRoot(input.CWD); gitRoot != "" {
|
if gitRoot := findGitRoot(input.CWD); gitRoot != "" {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue