mirror of
https://github.com/imjasonh/cnotes
synced 2026-07-22 07:39:54 +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
|
||||
func BlockRmRf(ctx context.Context, input hooks.HookInput) (hooks.HookOutput, error) {
|
||||
cmd := input.ToolUseRequest.Parameters["command"].(string)
|
||||
if strings.Contains(cmd, "rm -rf /") {
|
||||
bashInput, err := input.GetBashInput()
|
||||
if err != nil {
|
||||
return hooks.HookOutput{Decision: "approve"}, nil
|
||||
}
|
||||
|
||||
if strings.Contains(bashInput.Command, "rm -rf /") {
|
||||
return hooks.HookOutput{
|
||||
Decision: "block",
|
||||
Reason: "Cannot execute rm -rf on root directory",
|
||||
}, 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
|
||||
func ForceNonInteractiveSudo(ctx context.Context, input hooks.HookInput) (hooks.HookOutput, error) {
|
||||
cmd := input.ToolUseRequest.Parameters["command"].(string)
|
||||
if strings.Contains(cmd, "sudo") && !strings.Contains(cmd, "sudo -n") {
|
||||
bashInput, err := input.GetBashInput()
|
||||
if err != nil {
|
||||
return hooks.HookOutput{Decision: "approve"}, nil
|
||||
}
|
||||
|
||||
if strings.Contains(bashInput.Command, "sudo") && !strings.Contains(bashInput.Command, "sudo -n") {
|
||||
return hooks.HookOutput{
|
||||
Decision: "continue",
|
||||
Decision: "approve",
|
||||
ModifiedParameters: map[string]interface{}{
|
||||
"command": strings.ReplaceAll(cmd, "sudo", "sudo -n"),
|
||||
"command": strings.ReplaceAll(bashInput.Command, "sudo", "sudo -n"),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
return hooks.HookOutput{Decision: "continue"}, nil
|
||||
return hooks.HookOutput{Decision: "approve"}, nil
|
||||
}
|
||||
```
|
||||
|
||||
## Built-in Hooks
|
||||
|
||||
This package includes several example hooks:
|
||||
This package includes comprehensive hook implementations for all Claude Code events:
|
||||
|
||||
### 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.
|
||||
|
||||
### Post Tool Use
|
||||
|
|
@ -99,7 +107,16 @@ This package includes several example hooks:
|
|||
- **Project Context**: Adds git branch and project type information
|
||||
|
||||
### 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
|
||||
|
||||
|
|
|
|||
|
|
@ -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(" • 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(" • 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")
|
||||
|
||||
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
|
||||
|
||||
import (
|
||||
|
|
@ -5,10 +18,13 @@ import (
|
|||
)
|
||||
|
||||
func init() {
|
||||
// PreToolUse hooks - validation and security
|
||||
hooks.RegisterHook(hooks.EventPreToolUse, "Bash", ValidateBashCommand)
|
||||
hooks.RegisterHook(hooks.EventPreToolUse, "Write|Edit|MultiEdit", PreventSensitiveFileEdits)
|
||||
|
||||
// PostToolUse hooks - logging and automation
|
||||
hooks.RegisterHook(hooks.EventPostToolUse, "*", LogToolUsage)
|
||||
|
||||
// UserPromptSubmit hooks - context enhancement
|
||||
hooks.RegisterHook(hooks.EventUserPromptSubmit, "*", AddProjectContext)
|
||||
}
|
||||
|
|
@ -2,78 +2,54 @@ package handlers
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/imjasonh/hooks/internal/hooks"
|
||||
)
|
||||
|
||||
func init() {
|
||||
hooks.RegisterHook(hooks.EventNotification, "*", SpeakNotification)
|
||||
hooks.RegisterHook(hooks.EventNotification, "*", LogNotification)
|
||||
}
|
||||
|
||||
func SpeakNotification(ctx context.Context, input hooks.HookInput) (hooks.HookOutput, error) {
|
||||
// Only run on macOS
|
||||
if runtime.GOOS != "darwin" {
|
||||
return hooks.HookOutput{Decision: "approve"}, nil
|
||||
}
|
||||
|
||||
// Build the notification content for speech
|
||||
// LogNotification logs notification events for debugging and monitoring.
|
||||
// This hook is triggered when Claude needs permission to use a tool or when
|
||||
// prompt input has been idle for 60+ seconds.
|
||||
//
|
||||
// Common use cases:
|
||||
// - Debug notification flow
|
||||
// - 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 notificationType string
|
||||
|
||||
// Check if we have the direct message format (newer)
|
||||
if input.Message != "" {
|
||||
message = input.Message
|
||||
notificationType = "direct"
|
||||
} else if input.Notification.Permission != "" {
|
||||
// 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 != "" {
|
||||
// Regular notification (older format)
|
||||
message = input.Notification.Message
|
||||
notificationType = "message"
|
||||
} else {
|
||||
// No valid notification content
|
||||
slog.Debug("notification hook triggered with no message content", "session_id", input.SessionID)
|
||||
return hooks.HookOutput{Decision: "approve"}, nil
|
||||
}
|
||||
|
||||
// Speak the notification using macOS say command
|
||||
if _, err := exec.LookPath("say"); err == nil {
|
||||
// Build more informative spoken message
|
||||
var spokenMessage string
|
||||
if input.Notification.Permission != "" {
|
||||
spokenMessage = fmt.Sprintf("Claude is requesting permission for %s: %s", input.Notification.Tool, message)
|
||||
} else if input.Notification.Tool != "" {
|
||||
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 != "")
|
||||
}
|
||||
}
|
||||
slog.Info("notification received",
|
||||
"type", notificationType,
|
||||
"message", message,
|
||||
"tool", input.Notification.Tool,
|
||||
"permission", input.Notification.Permission != "",
|
||||
"session_id", input.SessionID,
|
||||
"timestamp", time.Now().Unix())
|
||||
|
||||
return hooks.HookOutput{Decision: "approve"}, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,31 +9,29 @@ import (
|
|||
)
|
||||
|
||||
func LogToolUsage(ctx context.Context, input hooks.HookInput) (hooks.HookOutput, error) {
|
||||
params := input.ToolUseRequest.Parameters
|
||||
|
||||
switch input.Tool {
|
||||
case "Bash":
|
||||
if cmd, ok := params["command"].(string); ok {
|
||||
if bashInput, err := input.GetBashInput(); err == nil {
|
||||
slog.Info("bash command executed",
|
||||
"command", cmd,
|
||||
"command", bashInput.Command,
|
||||
"session_id", input.SessionID,
|
||||
"cwd", input.CWD,
|
||||
"timestamp", time.Now().Unix())
|
||||
}
|
||||
case "Write", "Edit", "MultiEdit":
|
||||
if path, ok := params["file_path"].(string); ok {
|
||||
slog.Info("file modified",
|
||||
"tool", input.Tool,
|
||||
"file", path,
|
||||
"session_id", input.SessionID,
|
||||
"timestamp", time.Now().Unix())
|
||||
}
|
||||
case "Read":
|
||||
if path, ok := params["file_path"].(string); ok {
|
||||
slog.Info("file read",
|
||||
"file", path,
|
||||
"session_id", input.SessionID,
|
||||
"timestamp", time.Now().Unix())
|
||||
case "Write", "Edit", "MultiEdit", "Read":
|
||||
if fileInput, err := input.GetFileInput(); err == nil {
|
||||
if input.Tool == "Read" {
|
||||
slog.Info("file read",
|
||||
"file", fileInput.FilePath,
|
||||
"session_id", input.SessionID,
|
||||
"timestamp", time.Now().Unix())
|
||||
} else {
|
||||
slog.Info("file modified",
|
||||
"tool", input.Tool,
|
||||
"file", fileInput.FilePath,
|
||||
"session_id", input.SessionID,
|
||||
"timestamp", time.Now().Unix())
|
||||
}
|
||||
}
|
||||
default:
|
||||
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"
|
||||
"log/slog"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/imjasonh/hooks/internal/hooks"
|
||||
)
|
||||
|
||||
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
|
||||
|
||||
if gitRoot := findGitRoot(input.CWD); gitRoot != "" {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue