1
0
Fork 0
mirror of https://github.com/imjasonh/cnotes synced 2026-07-12 19:01:15 +00:00
cnotes/internal/handlers/pretooluse.go
Jason Hall c61b579243
Complete hooks implementation with comprehensive features
- Add project-level settings support (.claude/settings.json)
- Implement regex-based dangerous command detection
- Add typed tool input parsing (BashToolInput, FileToolInput)
- Replace terminal-notifier with speech-only notifications
- Add contextual spoken messages for prompts and notifications
- Implement timeout handling for hook execution
- Add comprehensive goimports automation for Go files
- Support global, local, and project hook configurations
- Fix JSON field mapping to match Claude Code expectations
- Add extensive logging and error handling

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-21 11:27:18 -04:00

106 lines
3.2 KiB
Go

package handlers
import (
"context"
"fmt"
"log/slog"
"regexp"
"strings"
"github.com/imjasonh/hooks/internal/hooks"
)
type DangerousPattern struct {
Pattern *regexp.Regexp
Description string
}
var dangerousPatterns = []DangerousPattern{
{regexp.MustCompile(`rm\s+-rf\s+/[^a-zA-Z]`), "recursive deletion of root filesystem"},
{regexp.MustCompile(`rm\s+-rf\s+/\*`), "recursive deletion of root filesystem contents"},
{regexp.MustCompile(`:\(\)\{\s*:\|\:&\s*\};\:`), "fork bomb"},
{regexp.MustCompile(`mkfs\.`), "filesystem formatting"},
{regexp.MustCompile(`dd\s+if=/dev/zero`), "disk wiping with dd"},
{regexp.MustCompile(`>\s*/dev/sd[a-z]`), "writing directly to disk device"},
{regexp.MustCompile(`wget\s+https?://`), "downloading files from internet"},
{regexp.MustCompile(`curl\s+https?://`), "downloading files from internet"},
{regexp.MustCompile(`chmod\s+\+x.*\.(sh|py|pl).*&&.*\./`), "download and execute pattern"},
{regexp.MustCompile(`sudo\s+rm\s+-rf`), "privileged recursive deletion"},
{regexp.MustCompile(`>/etc/passwd`), "overwriting system password file"},
{regexp.MustCompile(`>/etc/shadow`), "overwriting system shadow file"},
}
func ValidateBashCommand(ctx context.Context, input hooks.HookInput) (hooks.HookOutput, error) {
bashInput, err := input.GetBashInput()
if err != nil {
slog.Debug("no bash input found", "error", err)
return hooks.HookOutput{Decision: "approve"}, nil
}
if bashInput.Command == "" {
return hooks.HookOutput{Decision: "approve"}, nil
}
// Check against dangerous patterns
for _, pattern := range dangerousPatterns {
if pattern.Pattern.MatchString(bashInput.Command) {
slog.Warn("blocked dangerous command",
"command", bashInput.Command,
"reason", pattern.Description)
return hooks.HookOutput{
Decision: "block",
Reason: fmt.Sprintf("Command blocked: %s", pattern.Description),
}, nil
}
}
// Modify sudo commands to be non-interactive
if strings.Contains(bashInput.Command, "sudo") && !strings.Contains(bashInput.Command, "sudo -n") {
slog.Info("modifying sudo command to non-interactive")
return hooks.HookOutput{
Decision: "approve",
ModifiedParameters: map[string]any{
"command": strings.ReplaceAll(bashInput.Command, "sudo", "sudo -n"),
},
}, nil
}
return hooks.HookOutput{Decision: "approve"}, nil
}
var sensitiveFiles = []string{
".env",
".aws/credentials",
".ssh/id_rsa",
"secrets",
"password",
"token",
"key.pem",
}
func PreventSensitiveFileEdits(ctx context.Context, input hooks.HookInput) (hooks.HookOutput, error) {
fileInput, err := input.GetFileInput()
if err != nil {
slog.Debug("no file input found", "error", err)
return hooks.HookOutput{Decision: "approve"}, nil
}
if fileInput.FilePath == "" {
return hooks.HookOutput{Decision: "approve"}, nil
}
lowerPath := strings.ToLower(fileInput.FilePath)
for _, sensitive := range sensitiveFiles {
if strings.Contains(lowerPath, strings.ToLower(sensitive)) {
slog.Warn("blocked sensitive file edit",
"file", fileInput.FilePath,
"pattern", sensitive)
return hooks.HookOutput{
Decision: "block",
Reason: fmt.Sprintf("Cannot edit sensitive file: %s", fileInput.FilePath),
}, nil
}
}
return hooks.HookOutput{Decision: "approve"}, nil
}