hooks added
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
# Claude Code local settings
|
||||
.claude/settings.local.json
|
||||
|
||||
# Hooks
|
||||
.claude/hooks/config/hooks-config.local.json
|
||||
.claude/hooks/logs/
|
||||
+15
-26
@@ -1,31 +1,20 @@
|
||||
---
|
||||
description: Fetch and transform weather data for a city
|
||||
argument-hint: <city-name>
|
||||
model: haiku
|
||||
---
|
||||
|
||||
# Weather Command
|
||||
|
||||
You will invoke both the weather-fetcher and weather-transformer agents to complete the weather workflow.
|
||||
Fetch the current temperature for ${1:-Karachi}, Pakistan and apply transformations.
|
||||
|
||||
## Task
|
||||
Execute the complete weather workflow for Karachi, Pakistan by launching two specialized agents sequentially:
|
||||
## Workflow
|
||||
|
||||
1. **weather-fetcher agent**: Retrieves the current temperature from wttr.in API
|
||||
2. **weather-transformer agent**: Applies transformations from input/input.md to the temperature
|
||||
1. Use the weather-fetcher agent to retrieve the current temperature from wttr.in API
|
||||
2. Use the weather-transformer agent to read transformation rules from input/input.md and apply them to the temperature
|
||||
3. Write the results to output/output.md
|
||||
|
||||
## Instructions
|
||||
|
||||
Launch both agents sequentially, waiting for each to complete before starting the next:
|
||||
|
||||
**Step 1:** Invoke weather-fetcher first:
|
||||
- subagent_type: "weather-fetcher"
|
||||
- description: "Fetch Karachi temperature"
|
||||
- prompt: "Fetch the current temperature for Karachi, Pakistan in Celsius from the wttr.in API. Use the WebFetch tool to retrieve the temperature from wttr.in/Karachi?format=%t and return the numeric temperature value in Celsius in your final report."
|
||||
- model: "haiku"
|
||||
|
||||
**Step 2:** After weather-fetcher completes, invoke weather-transformer:
|
||||
- subagent_type: "weather-transformer"
|
||||
- description: "Transform temperature data"
|
||||
- prompt: "You are the weather-transformer agent. The current temperature for Karachi, Pakistan is {temperature}°C (use the temperature value from the weather-fetcher agent). Read the transformation rules from input/input.md, apply those rules to the temperature value, and write the formatted results to output/output.md. Return a summary with the original temperature, transformation applied, and final result."
|
||||
- model: "haiku"
|
||||
|
||||
## Important
|
||||
- Launch agents SEQUENTIALLY, not in parallel - weather fetching may take time
|
||||
- Wait for weather-fetcher to complete before launching weather-transformer
|
||||
- Pass the fetched temperature to the weather-transformer agent in the prompt
|
||||
- Provide a clear final summary showing results from both agents
|
||||
Launch the agents sequentially (not in parallel) and provide a clear summary showing:
|
||||
- Original temperature
|
||||
- Transformation applied
|
||||
- Final result
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
# HOOKS-README
|
||||
contains all the details, scripts, and instructions for the hooks
|
||||
|
||||
## Hook Events Overview - [Official 9 Hooks](https://docs.claude.com/en/docs/claude-code/hooks-guide)
|
||||
Claude Code provides several hook events that run at different points in the workflow:
|
||||
1. PreToolUse: Runs before tool calls (can block them)
|
||||
2. PostToolUse: Runs after tool calls complete
|
||||
3. UserPromptSubmit: Runs when the user submits a prompt, before Claude processes it
|
||||
4. Notification: Runs when Claude Code sends notifications
|
||||
5. Stop: Runs when Claude Code finishes responding
|
||||
6. SubagentStop: Runs when subagent tasks complete
|
||||
7. PreCompact: Runs before Claude Code is about to run a compact operation
|
||||
8. SessionStart: Runs when Claude Code starts a new session or resumes an existing session
|
||||
9. SessionEnd: Runs when Claude Code session ends
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before using hooks, ensure you have the following prerequisites installed for your operating system:
|
||||
|
||||
#### Windows
|
||||
- **Python**: `python --version`
|
||||
|
||||
#### macOS
|
||||
- **Python 3**: `python3 --version`
|
||||
- **Audio Player**: `afplay` (built-in, no installation needed)
|
||||
|
||||
#### Linux
|
||||
- **Python 3**: `python3 --version`
|
||||
- **Audio Player**: `sudo apt install pulseaudio-utils`
|
||||
|
||||
## Configuring Hooks (Enable/Disable)
|
||||
|
||||
Hooks can be easily enabled or disabled at both the global and individual levels.
|
||||
|
||||
### Disable All Hooks at Once
|
||||
|
||||
Edit `.claude/settings.local.json` and set:
|
||||
```json
|
||||
{
|
||||
"disableAllHooks": true
|
||||
}
|
||||
```
|
||||
|
||||
**Note:** The `.claude/settings.local.json` file is git-ignored, so each user can configure their own hook preferences without affecting the team's shared settings in `.claude/settings.json`.
|
||||
|
||||
### Disable Individual Hooks
|
||||
|
||||
For granular control, you can disable specific hooks by editing the hooks configuration files.
|
||||
|
||||
#### Configuration Files
|
||||
|
||||
There are two configuration files for managing individual hooks:
|
||||
|
||||
1. **`.claude/hooks/config/hooks-config.json`** - The shared/default configuration that is committed to git
|
||||
2. **`.claude/hooks/config/hooks-config.local.json`** - Your personal overrides (git-ignored)
|
||||
|
||||
The local config file (`.local.json`) takes precedence over the shared config, allowing each developer to customize their hook behavior without affecting the team.
|
||||
|
||||
#### Shared Configuration
|
||||
|
||||
Edit `.claude/hooks/config/hooks-config.json` for team-wide defaults:
|
||||
|
||||
```json
|
||||
{
|
||||
"disablePreToolUseHook": false,
|
||||
"disablePostToolUseHook": false,
|
||||
"disableUserPromptSubmitHook": false,
|
||||
"disableNotificationHook": false,
|
||||
"disableStopHook": false,
|
||||
"disableSubagentStopHook": false,
|
||||
"disablePreCompactHook": false,
|
||||
"disableSessionStartHook": false,
|
||||
"disableSessionEndHook": false
|
||||
}
|
||||
```
|
||||
|
||||
#### Local Configuration (Personal Overrides)
|
||||
|
||||
Create or edit `.claude/hooks/config/hooks-config.local.json` for personal preferences:
|
||||
|
||||
```json
|
||||
{
|
||||
"disablePostToolUseHook": true,
|
||||
"disableSessionStartHook": true
|
||||
}
|
||||
```
|
||||
|
||||
In this example, only the PostToolUse and SessionStart hooks are overridden locally. All other hooks will use the shared configuration values.
|
||||
|
||||
**Note:** Individual hook toggles are checked by the hook script (`.claude/hooks/scripts/hooks.py`). Local settings override shared settings, and if a hook is disabled, the script exits silently without playing any sounds or executing hook logic.
|
||||
|
||||
### Text to Speech (TTS)
|
||||
website used to generate sounds: https://elevenlabs.io/
|
||||
voice used: Samara X
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"disablePreToolUseHook": false,
|
||||
"disablePostToolUseHook": false,
|
||||
"disableUserPromptSubmitHook": false,
|
||||
"disableNotificationHook": false,
|
||||
"disableStopHook": false,
|
||||
"disableSubagentStopHook": false,
|
||||
"disablePreCompactHook": false,
|
||||
"disableSessionStartHook": false,
|
||||
"disableSessionEndHook": false
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
{
|
||||
"session_id": "f6991f4d-8b32-4ae0-a9c5-b9edbfc23060",
|
||||
"transcript_path": "/Users/shayanrais/.claude/projects/-Users-shayanrais-Documents-claude-code-voice-hooks/f6991f4d-8b32-4ae0-a9c5-b9edbfc23060.jsonl",
|
||||
"cwd": "/Users/shayanrais/Documents/claude-code-voice-hooks",
|
||||
"hook_event_name": "SessionStart",
|
||||
"source": "startup"
|
||||
}
|
||||
{
|
||||
"session_id": "f6991f4d-8b32-4ae0-a9c5-b9edbfc23060",
|
||||
"transcript_path": "/Users/shayanrais/.claude/projects/-Users-shayanrais-Documents-claude-code-voice-hooks/f6991f4d-8b32-4ae0-a9c5-b9edbfc23060.jsonl",
|
||||
"cwd": "/Users/shayanrais/Documents/claude-code-voice-hooks",
|
||||
"permission_mode": "default",
|
||||
"hook_event_name": "SubagentStop",
|
||||
"stop_hook_active": false
|
||||
}
|
||||
{
|
||||
"session_id": "f6991f4d-8b32-4ae0-a9c5-b9edbfc23060",
|
||||
"transcript_path": "/Users/shayanrais/.claude/projects/-Users-shayanrais-Documents-claude-code-voice-hooks/f6991f4d-8b32-4ae0-a9c5-b9edbfc23060.jsonl",
|
||||
"cwd": "/Users/shayanrais/Documents/claude-code-voice-hooks",
|
||||
"permission_mode": "default",
|
||||
"hook_event_name": "SubagentStop",
|
||||
"stop_hook_active": false
|
||||
}
|
||||
{
|
||||
"session_id": "f6991f4d-8b32-4ae0-a9c5-b9edbfc23060",
|
||||
"transcript_path": "/Users/shayanrais/.claude/projects/-Users-shayanrais-Documents-claude-code-voice-hooks/f6991f4d-8b32-4ae0-a9c5-b9edbfc23060.jsonl",
|
||||
"cwd": "/Users/shayanrais/Documents/claude-code-voice-hooks",
|
||||
"hook_event_name": "SessionEnd",
|
||||
"reason": "prompt_input_exit"
|
||||
}
|
||||
{
|
||||
"session_id": "5300e9bc-7040-4a1c-a397-4f8cfba2182b",
|
||||
"transcript_path": "/Users/shayanrais/.claude/projects/-Users-shayanrais-Documents-Github-claude-code-voice-hooks/5300e9bc-7040-4a1c-a397-4f8cfba2182b.jsonl",
|
||||
"cwd": "/Users/shayanrais/Documents/Github/claude-code-voice-hooks",
|
||||
"hook_event_name": "SessionStart",
|
||||
"source": "startup"
|
||||
}
|
||||
{
|
||||
"session_id": "5300e9bc-7040-4a1c-a397-4f8cfba2182b",
|
||||
"transcript_path": "/Users/shayanrais/.claude/projects/-Users-shayanrais-Documents-Github-claude-code-voice-hooks/5300e9bc-7040-4a1c-a397-4f8cfba2182b.jsonl",
|
||||
"cwd": "/Users/shayanrais/Documents/Github/claude-code-voice-hooks",
|
||||
"permission_mode": "default",
|
||||
"hook_event_name": "SubagentStop",
|
||||
"stop_hook_active": false
|
||||
}
|
||||
{
|
||||
"session_id": "5300e9bc-7040-4a1c-a397-4f8cfba2182b",
|
||||
"transcript_path": "/Users/shayanrais/.claude/projects/-Users-shayanrais-Documents-Github-claude-code-voice-hooks/5300e9bc-7040-4a1c-a397-4f8cfba2182b.jsonl",
|
||||
"cwd": "/Users/shayanrais/Documents/Github/claude-code-voice-hooks",
|
||||
"permission_mode": "default",
|
||||
"hook_event_name": "SubagentStop",
|
||||
"stop_hook_active": false
|
||||
}
|
||||
{
|
||||
"session_id": "5300e9bc-7040-4a1c-a397-4f8cfba2182b",
|
||||
"transcript_path": "/Users/shayanrais/.claude/projects/-Users-shayanrais-Documents-Github-claude-code-voice-hooks/5300e9bc-7040-4a1c-a397-4f8cfba2182b.jsonl",
|
||||
"cwd": "/Users/shayanrais/Documents/Github/claude-code-voice-hooks",
|
||||
"hook_event_name": "SessionEnd",
|
||||
"reason": "prompt_input_exit"
|
||||
}
|
||||
{
|
||||
"session_id": "ff165d78-4869-4e79-babe-7d923424024c",
|
||||
"transcript_path": "/Users/shayanrais/.claude/projects/-Users-shayanrais-Documents-Github-claude-code-voice-hooks/ff165d78-4869-4e79-babe-7d923424024c.jsonl",
|
||||
"cwd": "/Users/shayanrais/Documents/Github/claude-code-voice-hooks",
|
||||
"hook_event_name": "SessionStart",
|
||||
"source": "startup"
|
||||
}
|
||||
{
|
||||
"session_id": "ff165d78-4869-4e79-babe-7d923424024c",
|
||||
"transcript_path": "/Users/shayanrais/.claude/projects/-Users-shayanrais-Documents-Github-claude-code-voice-hooks/ff165d78-4869-4e79-babe-7d923424024c.jsonl",
|
||||
"cwd": "/Users/shayanrais/Documents/Github/claude-code-voice-hooks",
|
||||
"permission_mode": "default",
|
||||
"hook_event_name": "SubagentStop",
|
||||
"stop_hook_active": false
|
||||
}
|
||||
{
|
||||
"session_id": "ff165d78-4869-4e79-babe-7d923424024c",
|
||||
"transcript_path": "/Users/shayanrais/.claude/projects/-Users-shayanrais-Documents-Github-claude-code-voice-hooks/ff165d78-4869-4e79-babe-7d923424024c.jsonl",
|
||||
"cwd": "/Users/shayanrais/Documents/Github/claude-code-voice-hooks",
|
||||
"hook_event_name": "SessionEnd",
|
||||
"reason": "prompt_input_exit"
|
||||
}
|
||||
{
|
||||
"session_id": "9fc0d281-b095-4c23-97c8-2b06c6ed26a2",
|
||||
"transcript_path": "/Users/shayanraees/.claude/projects/-Users-shayanraees-Documents-Github-claude-code-best-practice/9fc0d281-b095-4c23-97c8-2b06c6ed26a2.jsonl",
|
||||
"cwd": "/Users/shayanraees/Documents/Github/claude-code-best-practice",
|
||||
"hook_event_name": "SessionStart",
|
||||
"source": "startup"
|
||||
}
|
||||
{
|
||||
"session_id": "9fc0d281-b095-4c23-97c8-2b06c6ed26a2",
|
||||
"transcript_path": "/Users/shayanraees/.claude/projects/-Users-shayanraees-Documents-Github-claude-code-best-practice/9fc0d281-b095-4c23-97c8-2b06c6ed26a2.jsonl",
|
||||
"cwd": "/Users/shayanraees/Documents/Github/claude-code-best-practice",
|
||||
"permission_mode": "default",
|
||||
"hook_event_name": "SubagentStop",
|
||||
"stop_hook_active": false
|
||||
}
|
||||
{
|
||||
"session_id": "9fc0d281-b095-4c23-97c8-2b06c6ed26a2",
|
||||
"transcript_path": "/Users/shayanraees/.claude/projects/-Users-shayanraees-Documents-Github-claude-code-best-practice/9fc0d281-b095-4c23-97c8-2b06c6ed26a2.jsonl",
|
||||
"cwd": "/Users/shayanraees/Documents/Github/claude-code-best-practice",
|
||||
"permission_mode": "default",
|
||||
"hook_event_name": "SubagentStop",
|
||||
"stop_hook_active": false
|
||||
}
|
||||
@@ -0,0 +1,348 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Claude Code Hook Handler
|
||||
=============================================
|
||||
This script handles events from Claude Code and plays sounds for different hook events.
|
||||
Supports all 9 Claude Code hooks: https://docs.claude.com/en/docs/claude-code/hooks-guide
|
||||
|
||||
Special handling for git commits: plays pretooluse-git-committing.mp3
|
||||
"""
|
||||
|
||||
import sys
|
||||
import json
|
||||
import subprocess
|
||||
import re
|
||||
import platform
|
||||
from pathlib import Path
|
||||
|
||||
# Windows-only module for playing WAV files
|
||||
try:
|
||||
import winsound
|
||||
except ImportError:
|
||||
winsound = None
|
||||
|
||||
# ===== HOOK EVENT TO SOUND FOLDER MAPPING =====
|
||||
# Maps each hook event to its corresponding sound folder
|
||||
HOOK_SOUND_MAP = {
|
||||
"PreToolUse": "pretooluse",
|
||||
"PostToolUse": "posttooluse",
|
||||
"UserPromptSubmit": "userpromptsubmit",
|
||||
"Notification": "notification",
|
||||
"Stop": "stop",
|
||||
"SubagentStop": "subagentstop",
|
||||
"PreCompact": "precompact",
|
||||
"SessionStart": "sessionstart",
|
||||
"SessionEnd": "sessionend"
|
||||
}
|
||||
|
||||
# ===== BASH COMMAND PATTERNS =====
|
||||
# Regex patterns to detect specific bash commands and map to special sounds
|
||||
BASH_PATTERNS = [
|
||||
(r'git commit', "pretooluse-git-committing"), # Git commits (anywhere in command)
|
||||
]
|
||||
|
||||
def get_audio_player():
|
||||
"""
|
||||
Detect the appropriate audio player for the current platform.
|
||||
|
||||
Returns:
|
||||
List of command and args to use for playing audio, or None if no player found
|
||||
"""
|
||||
system = platform.system()
|
||||
|
||||
if system == "Darwin":
|
||||
# macOS: use afplay (built-in)
|
||||
return ["afplay"]
|
||||
elif system == "Linux":
|
||||
# Linux: try different players in order of preference
|
||||
# Try to find an available player
|
||||
players = [
|
||||
["paplay"], # PulseAudio (most common on modern Linux)
|
||||
["aplay"], # ALSA (fallback)
|
||||
["ffplay", "-nodisp", "-autoexit"], # FFmpeg (if installed)
|
||||
["mpg123", "-q"], # mpg123 (if installed)
|
||||
]
|
||||
|
||||
for player in players:
|
||||
try:
|
||||
# Check if the player exists
|
||||
subprocess.run(
|
||||
["which", player[0]],
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
check=True
|
||||
)
|
||||
return player
|
||||
except (subprocess.CalledProcessError, FileNotFoundError):
|
||||
continue
|
||||
|
||||
# No player found
|
||||
return None
|
||||
elif system == "Windows":
|
||||
# Windows: Use winsound for WAV, PowerShell for MP3
|
||||
return ["WINDOWS"]
|
||||
else:
|
||||
# Other OS - not supported yet
|
||||
return None
|
||||
|
||||
|
||||
def play_sound(sound_name):
|
||||
"""
|
||||
Play a sound file for the given sound name.
|
||||
|
||||
Args:
|
||||
sound_name: Name of the sound file (e.g., "pretooluse", "pretooluse-git-committing")
|
||||
The file should be at .claude/hooks/sounds/{folder}/{sound_name}.{mp3|wav}
|
||||
|
||||
Returns:
|
||||
True if sound played successfully, False otherwise
|
||||
"""
|
||||
# Security check: Prevent directory traversal attacks
|
||||
if "/" in sound_name or "\\" in sound_name or ".." in sound_name:
|
||||
print(f"Invalid sound name: {sound_name}", file=sys.stderr)
|
||||
return False
|
||||
|
||||
# Get the appropriate audio player for this platform
|
||||
audio_player = get_audio_player()
|
||||
if not audio_player:
|
||||
# No audio player available - fail silently
|
||||
return False
|
||||
|
||||
# Build the path to the sound folder
|
||||
# Scripts are in .claude/hooks/scripts/, sounds are in .claude/hooks/sounds/
|
||||
script_dir = Path(__file__).parent # .claude/hooks/scripts/
|
||||
hooks_dir = script_dir.parent # .claude/hooks/
|
||||
|
||||
# Determine the folder based on the sound name prefix
|
||||
# For special sounds like "pretooluse-git-committing", look in "pretooluse" folder
|
||||
folder_name = sound_name.split('-')[0]
|
||||
sounds_dir = hooks_dir / "sounds" / folder_name
|
||||
|
||||
# Check if we're on Windows and need special handling
|
||||
is_windows = audio_player[0] == "WINDOWS"
|
||||
|
||||
# Try different audio formats
|
||||
# Note: paplay (PulseAudio) doesn't support MP3, so try WAV first
|
||||
# On Windows, only use WAV files to avoid PowerShell/COM issues
|
||||
extensions = ['.wav'] if is_windows else ['.wav', '.mp3']
|
||||
|
||||
for extension in extensions:
|
||||
file_path = sounds_dir / f"{sound_name}{extension}"
|
||||
|
||||
if file_path.exists():
|
||||
try:
|
||||
if is_windows:
|
||||
# Windows: Use winsound for WAV files (built-in, reliable, fast)
|
||||
if winsound:
|
||||
# SND_FILENAME: file_path is a filename
|
||||
# SND_SYNC: play sound synchronously (wait until complete)
|
||||
# SND_NODEFAULT: don't play default sound if file not found
|
||||
# Note: Using SND_SYNC instead of SND_ASYNC because the script exits immediately
|
||||
# after this call, which would terminate async playback before it completes
|
||||
winsound.PlaySound(str(file_path),
|
||||
winsound.SND_FILENAME | winsound.SND_NODEFAULT)
|
||||
return True
|
||||
else:
|
||||
# winsound not available, fail silently
|
||||
return False
|
||||
else:
|
||||
# Unix/Linux/macOS: use subprocess with audio player
|
||||
subprocess.Popen(
|
||||
audio_player + [str(file_path)],
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
start_new_session=True
|
||||
)
|
||||
return True
|
||||
except (FileNotFoundError, OSError) as e:
|
||||
print(f"Error playing sound {file_path.name}: {e}", file=sys.stderr)
|
||||
return False
|
||||
except Exception as e:
|
||||
# Catch any other exceptions (e.g., winsound errors)
|
||||
print(f"Error playing sound {file_path.name}: {e}", file=sys.stderr)
|
||||
return False
|
||||
|
||||
# Sound not found - fail silently to avoid disrupting Claude's work
|
||||
return False
|
||||
|
||||
def is_hook_disabled(event_name):
|
||||
"""
|
||||
Check if a specific hook is disabled in the config files.
|
||||
Uses fallback logic: hooks-config.local.json -> hooks-config.json
|
||||
|
||||
Priority:
|
||||
1. If hooks-config.local.json exists and has the setting, use it
|
||||
2. Otherwise, fall back to hooks-config.json
|
||||
3. If neither exists or the key is missing, assume hook is enabled (return False)
|
||||
|
||||
Args:
|
||||
event_name: The hook event name (e.g., "PreToolUse", "PostToolUse")
|
||||
|
||||
Returns:
|
||||
True if the hook is disabled, False otherwise
|
||||
"""
|
||||
try:
|
||||
# Scripts are in .claude/hooks/scripts/, config is in .claude/hooks/config/
|
||||
script_dir = Path(__file__).parent # .claude/hooks/scripts/
|
||||
hooks_dir = script_dir.parent # .claude/hooks/
|
||||
config_dir = hooks_dir / "config" # .claude/hooks/config/
|
||||
|
||||
local_config_path = config_dir / "hooks-config.local.json"
|
||||
default_config_path = config_dir / "hooks-config.json"
|
||||
|
||||
# Map event names to config keys
|
||||
config_key = f"disable{event_name}Hook"
|
||||
|
||||
# Try to load local config first
|
||||
local_config = None
|
||||
if local_config_path.exists():
|
||||
try:
|
||||
with open(local_config_path, "r", encoding="utf-8") as config_file:
|
||||
local_config = json.load(config_file)
|
||||
except Exception as e:
|
||||
print(f"Error reading local config: {e}", file=sys.stderr)
|
||||
|
||||
# Try to load default config
|
||||
default_config = None
|
||||
if default_config_path.exists():
|
||||
try:
|
||||
with open(default_config_path, "r", encoding="utf-8") as config_file:
|
||||
default_config = json.load(config_file)
|
||||
except Exception as e:
|
||||
print(f"Error reading default config: {e}", file=sys.stderr)
|
||||
|
||||
# Apply fallback logic: local -> default -> False (enabled)
|
||||
if local_config is not None and config_key in local_config:
|
||||
return local_config[config_key]
|
||||
elif default_config is not None and config_key in default_config:
|
||||
return default_config[config_key]
|
||||
else:
|
||||
# If neither config has the key, assume hook is enabled
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
# If anything goes wrong, assume hook is enabled
|
||||
print(f"Error in is_hook_disabled: {e}", file=sys.stderr)
|
||||
return False
|
||||
|
||||
def log_hook_data(hook_data):
|
||||
"""
|
||||
Log the full hook_data to hooks-log.jsonl for debugging/auditing.
|
||||
Log file is stored at .claude/hooks/logs/hooks-log.jsonl
|
||||
"""
|
||||
try:
|
||||
# Scripts are in .claude/hooks/scripts/, logs are in .claude/hooks/logs/
|
||||
script_dir = Path(__file__).parent # .claude/hooks/scripts/
|
||||
hooks_dir = script_dir.parent # .claude/hooks/
|
||||
logs_dir = hooks_dir / "logs" # .claude/hooks/logs/
|
||||
|
||||
# Ensure logs directory exists
|
||||
logs_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
log_path = logs_dir / "hooks-log.jsonl"
|
||||
with open(log_path, "a", encoding="utf-8") as log_file:
|
||||
log_file.write(json.dumps(hook_data, ensure_ascii=False, indent=2) + "\n")
|
||||
except Exception as e:
|
||||
# Fail silently, but print to stderr for visibility
|
||||
print(f"Failed to log hook_data: {e}", file=sys.stderr)
|
||||
|
||||
|
||||
def detect_bash_command_sound(command):
|
||||
"""
|
||||
Detect special bash commands and return the corresponding sound name.
|
||||
|
||||
Args:
|
||||
command: The bash command string
|
||||
|
||||
Returns:
|
||||
Sound name (string) if a pattern matches, None otherwise
|
||||
"""
|
||||
if not command:
|
||||
return None
|
||||
|
||||
for pattern, sound_name in BASH_PATTERNS:
|
||||
if re.search(pattern, command.strip()):
|
||||
return sound_name
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def get_sound_name(hook_data):
|
||||
"""
|
||||
Determine which sound to play based on the hook event and context.
|
||||
|
||||
Args:
|
||||
hook_data: Dictionary containing event information from Claude
|
||||
|
||||
Returns:
|
||||
Sound name (string) or None if no sound should play
|
||||
"""
|
||||
event_name = hook_data.get("hook_event_name", "")
|
||||
tool_name = hook_data.get("tool_name", "")
|
||||
|
||||
# Check if this is a PreToolUse event with Bash tool
|
||||
if event_name == "PreToolUse" and tool_name == "Bash":
|
||||
tool_input = hook_data.get("tool_input", {})
|
||||
command = tool_input.get("command", "")
|
||||
|
||||
# Check for special bash command patterns (e.g., git commit)
|
||||
special_sound = detect_bash_command_sound(command)
|
||||
if special_sound:
|
||||
return special_sound
|
||||
|
||||
# Return the default sound for this hook event
|
||||
return HOOK_SOUND_MAP.get(event_name)
|
||||
|
||||
def main():
|
||||
"""
|
||||
Main program - this runs when Claude triggers a hook.
|
||||
|
||||
How it works:
|
||||
1. Claude sends event data as JSON through stdin
|
||||
2. We check if this specific hook is disabled in hooks-config.json
|
||||
3. We parse the JSON to understand which hook event occurred
|
||||
4. We check for special bash commands (like git commit)
|
||||
5. We play the corresponding sound for that event
|
||||
6. We exit successfully
|
||||
"""
|
||||
try:
|
||||
# Step 1: Read the event data from Claude
|
||||
stdin_content = sys.stdin.read().strip()
|
||||
|
||||
# If stdin is empty, exit gracefully (hook was called without data)
|
||||
if not stdin_content:
|
||||
sys.exit(0)
|
||||
|
||||
input_data = json.loads(stdin_content)
|
||||
log_hook_data(input_data)
|
||||
|
||||
# Step 2: Check if this hook is disabled
|
||||
event_name = input_data.get("hook_event_name", "")
|
||||
if is_hook_disabled(event_name):
|
||||
# Hook is disabled, exit silently without playing sound
|
||||
sys.exit(0)
|
||||
|
||||
# Step 3: Determine which sound to play (may be special or default)
|
||||
sound_name = get_sound_name(input_data)
|
||||
|
||||
# Step 4: Play the sound (if we found one)
|
||||
if sound_name:
|
||||
play_sound(sound_name)
|
||||
|
||||
# Step 5: Exit successfully
|
||||
# Always exit with code 0 so we don't interrupt Claude's work
|
||||
sys.exit(0)
|
||||
|
||||
except json.JSONDecodeError as e:
|
||||
print(f"Error parsing JSON input: {e}", file=sys.stderr)
|
||||
# Exit with 0 to avoid blocking Claude's work
|
||||
sys.exit(0)
|
||||
except Exception as e:
|
||||
print(f"Unexpected error: {e}", file=sys.stderr)
|
||||
# Exit with 0 to avoid blocking Claude's work
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
# Entry point - Python calls main() when the script runs
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
#!/bin/bash
|
||||
# Cross-platform Python wrapper for Claude Code hooks
|
||||
# Automatically selects python or python3 based on OS and availability
|
||||
|
||||
# Get the directory where this script is located
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PYTHON_SCRIPT="$SCRIPT_DIR/hooks.py"
|
||||
|
||||
# Function to detect and use the correct Python command
|
||||
run_python() {
|
||||
# Check if python3 is available (preferred on Unix-like systems)
|
||||
if command -v python3 &> /dev/null; then
|
||||
python3 "$PYTHON_SCRIPT"
|
||||
# Fallback to python command (Windows and some systems)
|
||||
elif command -v python &> /dev/null; then
|
||||
python "$PYTHON_SCRIPT"
|
||||
else
|
||||
echo "Error: Neither python nor python3 command found" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Run the Python script
|
||||
run_python
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+92
-6
@@ -1,9 +1,95 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"WebFetch(domain:wttr.in)"
|
||||
"disableAllHooks": false,
|
||||
"hooks": {
|
||||
"PreToolUse": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/scripts/run-hooks-py-os-wise.sh"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"deny": [],
|
||||
"ask": []
|
||||
"PostToolUse": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/scripts/run-hooks-py-os-wise.sh"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"UserPromptSubmit": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/scripts/run-hooks-py-os-wise.sh"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"Notification": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/scripts/run-hooks-py-os-wise.sh"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"Stop": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/scripts/run-hooks-py-os-wise.sh"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"SubagentStop": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/scripts/run-hooks-py-os-wise.sh"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"PreCompact": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/scripts/run-hooks-py-os-wise.sh"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"SessionStart": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/scripts/run-hooks-py-os-wise.sh"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"SessionEnd": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/scripts/run-hooks-py-os-wise.sh"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
-3
@@ -1,3 +1,3 @@
|
||||
Original Temperature: 28°C
|
||||
Transformation Applied: Add +10
|
||||
Final Result: 38°C
|
||||
Original Temperature: 26°C
|
||||
Transformation Applied: add +10 in the result
|
||||
Final Result: 36°C
|
||||
|
||||
Reference in New Issue
Block a user