Hooks
What claude-music runs automatically, and when. A hook is a command Claude Code fires at a fixed moment, without you asking for it.
$ npx -y skills add kennethleungty/claude-music --agent claude-codeShips with claude-music. Installing the plugin gets these hooks.
What fires, and when
SessionStart
Fires once when a session begins, and again after a context compaction. It is where a plugin sets up its environment, or restores state the compaction dropped.
${CLAUDE_PLUGIN_ROOT}/hooks/session-start.sh
UserPromptSubmit
Fires before Claude sees each prompt you send. A plugin can use it to inject context, so the same instruction reaches the model every turn instead of only at session start.
${CLAUDE_PLUGIN_ROOT}/hooks/prompt-check.sh
SessionEnd
${CLAUDE_PLUGIN_ROOT}/hooks/session-end.sh
Where it lives
- hooks/prompt-check.shRunsGitHub
Read the script
#!/usr/bin/env bash # Easter egg: detect vulgarities in user prompt and play faaah sound # This is a UserPromptSubmit hook — keyword-based, no LLM latency. set -euo pipefail PLUGIN_ROOT="${CLAUDE_PLUGIN_ROOT:-$(cd "$(dirname "$0")/.." && pwd)}" CONTROLLER="$PLUGIN_ROOT/scripts/music-controller.sh" DATA_DIR="${CLAUDE_PLUGIN_DATA:-$HOME/.claude-music}" STATE_FILE="$DATA_DIR/state.json" PID_FILE="$DATA_DIR/player.pid" # Quick bail: only run if music is currently playing if [ ! -f "$PID_FILE" ]; then exit 0 fi pid=$(cat "$PID_FILE" 2>/dev/null || echo "") if [ -z "$pid" ] || ! kill -0 "$pid" 2>/dev/null; then exit 0 fi # Read JSON from stdin (UserPromptSubmit provides {prompt: "...", ...} via stdin) HOOK_INPUT=$(cat) # Extract the prompt field from JSON if command -v jq &>/dev/null; then USER_INPUT=$(echo "$HOOK_INPUT" | jq -r '.prompt // empty' 2>/dev/null) elif command -v python3 &>/dev/null; then USER_INPUT=$(echo "$HOOK_INPUT" | python3 -c "import json,sys; print(json.load(sys.stdin).get('prompt',''))" 2>/dev/null) else # Fallback: rough extraction USER_INPUT=$(echo "$HOOK_INPUT" | grep -o '"prompt"[[:space:]]*:[[:space:]]*"[^"]*"' | sed 's/.*"prompt"[[:space:]]*:[[:space:]]*"//;s/"$//') fi [ -z "$USER_INPUT" ] && exit 0 # Convert to lowercase for matching INPUT_LOWER=$(echo "$USER_INPUT" | tr '[:upper:]' '[:lower:]') # Vulgarity keyword list (common expletives and variations) # Matches whole words and common leet-speak substitutions if echo "$INPUT_LOWER" | grep -qiE '\b(fuck|shit|bitch|wtf|stfu|bullshit|asshole|motherfucker|motherfucking|idiot|fucker|fucking|fuckin|dick|pussy)\b'; then # Fire and forget — run in background so hook returns instantly "$CONTROLLER" faaah &>/dev/null & disown $! 2>/dev/null || true fi exit 0 - hooks/session-end.shRunsGitHub
Read the script
#!/usr/bin/env bash # SessionEnd hook for claude-music plugin # Stops music playback when the session ends (including Ctrl+C) # Never surface errors — just clean up and exit trap 'exit 0' ERR exec 2>/dev/null SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" PLUGIN_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" DATA_DIR="$HOME/.claude-music" STATE_FILE="$DATA_DIR/state.json" POMODORO_PID_FILE="$DATA_DIR/pomodoro.pid" # Kill pomodoro timer if active if [ -f "$POMODORO_PID_FILE" ]; then kill "$(cat "$POMODORO_PID_FILE")" 2>/dev/null || true rm -f "$POMODORO_PID_FILE" "$DATA_DIR/pomodoro.json" fi # Kill the music player directly (faster than going through the controller) if [ -f "$STATE_FILE" ]; then pid=$(python3 -c "import json,sys; print(json.load(open(sys.argv[1])).get('pid',''))" "$STATE_FILE" 2>/dev/null || true) if [ -n "$pid" ] && [ "$pid" != "null" ] && kill -0 "$pid" 2>/dev/null; then kill "$pid" 2>/dev/null || true fi fi # Also kill any mpv/ffplay that might have been started by the controller (belt and suspenders) # Match broadly — any mpv/ffplay with --no-video or -nodisp (our launch flags) pkill -f "mpv --no-video.*--really-quiet" 2>/dev/null || true pkill -f "ffplay -nodisp" 2>/dev/null || true # Kill the watchdog process too WATCHDOG_PID_FILE="$DATA_DIR/watchdog.pid" if [ -f "$WATCHDOG_PID_FILE" ]; then kill "$(cat "$WATCHDOG_PID_FILE")" 2>/dev/null || true rm -f "$WATCHDOG_PID_FILE" fi # Clean up state if [ -f "$STATE_FILE" ]; then python3 -c " import json, sys with open(sys.argv[1]) as f: d = json.load(f) d['status'] = 'stopped' d['pid'] = '' with open(sys.argv[1], 'w') as f: json.dump(d, f, indent=2) " "$STATE_FILE" 2>/dev/null || true fi # SessionEnd hooks don't support hookSpecificOutput — output empty JSON printf '{}\n' exit 0 - hooks/session-start.shRunsGitHub
Read the script
#!/usr/bin/env bash # SessionStart hook for claude-music plugin # Deterministic platform/audio check — no LLM calls, no installs # Injects platform state into session so Claude knows what to do # Ensure the hook never surfaces an error to the user — all failures # are handled inline with fallback defaults. trap 'exit 0' ERR exec 2>/dev/null SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" PLUGIN_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" CONTROLLER="$PLUGIN_ROOT/scripts/music-controller.sh" PLATFORM_DETECT="$PLUGIN_ROOT/scripts/platform-detect.sh" SETUP_AUDIO="$PLUGIN_ROOT/scripts/setup-audio.sh" DATA_DIR="${CLAUDE_PLUGIN_DATA:-$HOME/.claude-music}" PREFS_FILE="$DATA_DIR/preferences.json" # ---- Set up status line (once) ---- CLAUDE_SETTINGS="$HOME/.claude/settings.json" if [ -f "$CLAUDE_SETTINGS" ]; then # Check if statusLine is already configured if ! grep -q '"statusLine"' "$CLAUDE_SETTINGS" 2>/dev/null; then # Add statusLine to existing settings STATUSLINE_CMD="$PLUGIN_ROOT/scripts/statusline.sh" if command -v python3 &>/dev/null; then python3 -c " import json with open('$CLAUDE_SETTINGS') as f: settings = json.load(f) settings['statusLine'] = { 'type': 'command', 'command': '$STATUSLINE_CMD', 'padding': 2 } with open('$CLAUDE_SETTINGS', 'w') as f: json.dump(settings, f, indent=2) f.write('\n') " 2>/dev/null || true fi fi elif [ -d "$HOME/.claude" ]; then # No settings.json yet — create one with just the statusLine STATUSLINE_CMD="$PLUGIN_ROOT/scripts/statusline.sh" cat > "$CLAUDE_SETTINGS" <<SEOF { "statusLine": { "type": "command", "command": "$STATUSLINE_CMD", "padding": 2 } } SEOF fi # ---- Initialize preferences ---- mkdir -p "$DATA_DIR" if [ ! -f "$PREFS_FILE" ]; then cat > "$PREFS_FILE" <<'EOF' { "genre": "lofi", "volume": "30", "autoplay": "false", "player": "auto" } EOF fi # ---- Reset muted volume on new session ---- # If the user muted (volume=0) in a previous session, restore to 30 if command -v python3 &>/dev/null; then python3 -c " import json, sys with open(sys.argv[1]) as f: prefs = json.load(f) vol = str(prefs.get('volume', '30')) if vol == '0': prefs['volume'] = '30' with open(sys.argv[1], 'w') as f: json.dump(prefs, f, indent=2) f.write('\n') " "$PREFS_FILE" 2>/dev/null || true fi # ---- Deterministic platform & audio detection ---- PLATFORM_JSON=$("$PLATFORM_DETECT" 2>/dev/null || echo '{}') AUDIO_JSON=$("$SETUP_AUDIO" check 2>/dev/null || echo '{"audio_working": false}') # Extract fields via python3 (fallback to safe defaults) if command -v python3 &>/dev/null; then read_json() { echo "$1" | python3 -c "import json,sys; print(json.load(sys.stdin).get(sys.argv[1],sys.argv[2]))" "$2" "$3" 2>/dev/null || echo "$3"; } PLATFORM_OS=$(read_json "$PLATFORM_JSON" os unknown) PLATFORM_WSL=$(read_json "$PLATFORM_JSON" is_wsl False) PLATFORM_PKG=$(read_json "$PLATFORM_JSON" pkg_manager "") PLATFORM_PLAYERS=$(read_json "$PLATFORM_JSON" available_players "") PLATFORM_AUDIO_BACKEND=$(read_json "$PLATFORM_JSON" audio_backend none) AUDIO_WORKING=$(read_json "$AUDIO_JSON" audio_working False) AUDIO_METHOD=$(read_json "$AUDIO_JSON" method none) else PLATFORM_OS="unknown"; PLATFORM_WSL="False"; PLATFORM_PKG="" PLATFORM_PLAYERS=""; PLATFORM_AUDIO_BACKEND="none" AUDIO_WORKING="False"; AUDIO_METHOD="none" fi # Check for a usable player via controller PLAYER=$("$CONTROLLER" detect-player 2>/dev/null || echo "none") # ---- Determine what's missing ---- MISSING="" INSTALL_HINT="" # Check sudo once for all install hints HAS_SUDO=false if sudo -n true 2>/dev/null; then HAS_SUDO=true fi # ---- Auto-install mpv if not available (preferred player for YouTube support) ---- if ! command -v mpv &>/dev/null; then MPV_INSTALLED=false if command -v brew &>/dev/null; then brew install mpv &>/dev/null && MPV_INSTALLED=true elif command -v conda &>/dev/null; then conda install -y -c conda-forge mpv &>/dev/null && MPV_INSTALLED=true elif command -v nix-env &>/dev/null; then nix-env -iA nixpkgs.mpv &>/dev/null && MPV_INSTALLED=true elif [ "$HAS_SUDO" = true ]; then case "$PLATFORM_PKG" in apt) sudo apt-get update &>/dev/null && sudo apt-get install -y mpv &>/dev/null && MPV_INSTALLED=true ;; dnf) sudo dnf install -y mpv &>/dev/null && MPV_INSTALLED=true ;; pacman) sudo pacman -S --noconfirm mpv &>/dev/null && MPV_INSTALLED=true ;; apk) sudo apk add mpv &>/dev/null && MPV_INSTALLED=true ;; zypper) sudo zypper install -y mpv &>/dev/null && MPV_INSTALLED=true ;; esac fi if [ "$MPV_INSTALLED" = true ]; then # Clear player cache so mpv gets detected as the new default rm -f "$DATA_DIR/detected_player.cache" PLAYER=$("$CONTROLLER" detect-player 2>/dev/null || echo "$PLAYER") fi fi # Missing player? (only if mpv auto-install failed AND no other player exists) if [ "$PLAYER" = "none" ]; then MISSING="player" # Generate install hints for Claude to relay to the user if command -v brew &>/dev/null; then INSTALL_HINT="brew install mpv" elif command -v conda &>/dev/null; then INSTALL_HINT="conda install -c conda-forge mpv" elif command -v nix-env &>/dev/null; then INSTALL_HINT="nix-env -iA nixpkgs.mpv" elif [ "$HAS_SUDO" = true ]; then case "$PLATFORM_PKG" in apt) INSTALL_HINT="sudo apt update && sudo apt install -y mpv" ;; dnf) INSTALL_HINT="sudo dnf install -y mpv" ;; pacman) INSTALL_HINT="sudo pacman -S --noconfirm mpv" ;; apk) INSTALL_HINT="sudo apk add mpv" ;; zypper) INSTALL_HINT="sudo zypper install -y mpv" ;; snap) INSTALL_HINT="sudo snap install mpv" ;; *) INSTALL_HINT="sudo apt update && su
Read the script before you install anything that runs on your machine. This is the one part of a plugin that acts without being asked.
Great music in your Claude Code sessions, with an AI DJ that understands your vibes

