Skip to content
Productivity
Hook

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.

From plugin
claude-music
2321 skills2 agents3 hooks
Install
$ npx -y skills add kennethleungty/claude-music --agent claude-code

Ships 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
Read hooks/hooks.json

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.

Ships withclaude-music

Great music in your Claude Code sessions, with an AI DJ that understands your vibes

Get the whole plugin
Stats
23
Stars
0
Forks
Maintained
Maintenance
Shell
Language
MIT
License
5mo ago
Last commit
6mo ago
Created

Repo: kennethleungty/claude-music