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
2621 skills2 agents3 hooks

Claude Code

$ npx -y skills add kennethleungty/claude-music --agent claude-code

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.

@kennethleungty
Ships withclaude-music

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

2621 skills2 agents
Get the whole plugin

Stars on GitHub

Momentum

stars in total
26
Star history of kennethleungty/claude-music on GitHub, a chart by star-history.com

More plugins that ship hooks.

i-have-adhd

i-have-adhd

A skill that shapes Claude Code's output for readers with ADHD: it leads with the next action, numbers multi-step work, restates state across turns, drops tangents, gives specific time estimates, and makes progress visible. Invoke it with /i-h.

1 skill1 hook60k
@ayghri@ayghri
Install Plugin
pua

pua

A Claude Code plugin that makes the assistant work harder when it stalls or repeats failures. It adds pressure-style coaching, plus skills for workplace reminders and senior engineer, CTO and team-lead modes for planning and execution.

12 skills+320k
@tanweai@tanweai
Install Plugin
claude-obsidian

claude-obsidian

Turns the sources you read, such as papers, articles and transcripts, into a linked wiki of Markdown notes in Obsidian. Claude keeps sources and citations so every summary can be traced back to its origin.

15 skills+216k
@agricidaniel@agricidaniel
Install Plugin
obsidian-second-brain

obsidian-second-brain

Gives Claude Code a persistent second brain stored as plain markdown in an Obsidian vault. Notes rewrite themselves as new sources arrive, and scheduled agents handle research synthesis and note capture.

1 skill+25.3k
@eugeniughelbur@eugeniughelbur
Install Plugin
arscontexta

arscontexta

A Claude Code plugin that builds a personal knowledge system through conversation, with skills for adding knowledge domains, checking vault health, getting methodology guidance, and evolving the system over time.

10 skills+23.5k
@agenticnotetaking@agenticnotetaking
Install Plugin
pro-workflow

pro-workflow

A Claude Code workflow plugin that learns from your corrections and keeps a self-correcting memory across sessions. It adds skills for agent teams, parallel worktree batch changes, bug capture as GitHub issues, project auto-setup and context compaction.

41 skills+32.9k
@rohitg00@rohitg00
Install Plugin
engram

engram

Helps you learn any topic with a first-principles curriculum, guided tutoring, and spaced review that schedules free-recall practice. Includes coaching for retention stats and study strategy.

3 skills+21.5k
@nagisanzenin@nagisanzenin
Install Plugin
hey

hey

A command line tool for HEY that lets you read and send email, manage contacts, labels, calendars, todos, habits, time tracking and journal entries from the terminal or from your coding agent.

1 skill1 hook423
@basecamp@basecamp
Install Plugin

Questions about the hooks in claude-music.

Which coding agents can use the hooks in claude-music?
The hooks in claude-music install in Claude Code and Cursor: each runs Claude Code hooks from .claude/settings.json, so copy the hooks there.