Skip to content
Productivity
Hook

Hooks

What sound-fx runs automatically, and when. A hook is a command Claude Code fires at a fixed moment, without you asking for it.

From plugin
sound-fx
451 command7 hooks
Install
> /plugin marketplace add 6m1w/claude-sound-fx
> /plugin install sound-fx@claude-sound-fx

Ships with sound-fx. 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.

  • bash ${CLAUDE_PLUGIN_ROOT}/hooks/hook.sh start

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.

  • bash ${CLAUDE_PLUGIN_ROOT}/hooks/hook.sh submit

Stop

  • bash ${CLAUDE_PLUGIN_ROOT}/hooks/hook.sh complete

PostToolUseFailure

  • bash ${CLAUDE_PLUGIN_ROOT}/hooks/hook.sh error

Notification

  • bash ${CLAUDE_PLUGIN_ROOT}/hooks/hook.sh notification

PreCompact

  • bash ${CLAUDE_PLUGIN_ROOT}/hooks/hook.sh precompact

SessionEnd

  • bash ${CLAUDE_PLUGIN_ROOT}/hooks/hook.sh session_end
Read hooks/hooks.json

In the plugin's words

How sound-fx describes its own hook set.

Sound effects triggered by Claude Code lifecycle events.

Where it lives

  • hooks/hook.shRunsGitHub
    Read the script
    #!/bin/bash
    # Claude Code hook event handler.
    # Usage: hook.sh <event_type>
    #
    # Reads user config from ~/.claude/sound-fx.local.json:
    #   theme:   "mix" | "jarvis" | "glados" | ... (default: mix)
    #   mode:    "full" | "minimal"                (default: full)
    #   enabled: true | false                      (default: true)
    #
    # Each theme directory under assets/ contains a manifest.json
    # that maps event names to sound files. Adding a new theme =
    # adding a new directory with manifest.json + wav files.
    #
    # Environment: CLAUDE_SOUND_VOLUME (0-100, default 60)
    
    # Resolve plugin root
    if [ -n "$CLAUDE_PLUGIN_ROOT" ]; then
      PLUGIN_ROOT="$CLAUDE_PLUGIN_ROOT"
    else
      PLUGIN_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
    fi
    
    ASSETS_DIR="$PLUGIN_ROOT/assets"
    CONFIG_FILE="$HOME/.claude/sound-fx.local.json"
    SOUND_VOLUME=${CLAUDE_SOUND_VOLUME:-60}
    SOUND_PORT=${CLAUDE_SOUND_PORT:-19876}
    
    EVENT="$1"
    [ -z "$EVENT" ] && exit 0
    
    # Sanitize event name
    EVENT=$(echo "$EVENT" | tr -cd 'a-zA-Z0-9_')
    [ -z "$EVENT" ] && exit 0
    
    # Read user config
    THEME="mix"
    MODE="full"
    ENABLED="True"
    if [ -f "$CONFIG_FILE" ]; then
      THEME=$(python3 -c "
    import json
    c = json.load(open('$CONFIG_FILE'))
    print(c.get('theme', 'mix'))
    " 2>/dev/null || echo "mix")
      MODE=$(python3 -c "
    import json
    c = json.load(open('$CONFIG_FILE'))
    print(c.get('mode', 'full'))
    " 2>/dev/null || echo "full")
      ENABLED=$(python3 -c "
    import json
    c = json.load(open('$CONFIG_FILE'))
    print(c.get('enabled', True))
    " 2>/dev/null || echo "True")
    fi
    
    # Disabled mode: exit silently
    [ "$ENABLED" = "False" ] && exit 0
    
    # Minimal mode: only essential events
    if [ "$MODE" = "minimal" ]; then
      case "$EVENT" in
        start|complete|notification) ;;
        *) exit 0 ;;
      esac
    fi
    
    # Detect audio player: local player if available, otherwise relay
    PLAYER=""
    IS_WSL=false
    if [ "$(uname)" = "Darwin" ]; then
      PLAYER="afplay"
    else
      # Check for WSL — try Windows-side players first
      if grep -qi microsoft /proc/version 2>/dev/null; then
        IS_WSL=true
        if command -v ffplay.exe >/dev/null 2>&1; then
          PLAYER="ffplay.exe"
        elif command -v powershell.exe >/dev/null 2>&1; then
          PLAYER="powershell.exe"
        fi
      fi
      # Linux native players (also works if WSL has PulseAudio via WSLg)
      if [ -z "$PLAYER" ]; then
        for cmd in paplay ffplay aplay; do
          if command -v "$cmd" >/dev/null 2>&1; then
            PLAYER="$cmd"
            break
          fi
        done
      fi
    fi
    
    # No local player found — forward to relay (remote SSH / headless)
    if [ -z "$PLAYER" ]; then
      curl -s --connect-timeout 1 "http://127.0.0.1:${SOUND_PORT}/${EVENT}" &>/dev/null &
      exit 0
    fi
    
    # Collect candidates from manifest.json files
    # If theme=mix, scan all directories; otherwise only the matching one
    CANDIDATES=$(python3 -c "
    import json, os, sys
    
    assets_dir = '$ASSETS_DIR'
    theme = '$THEME'
    event = '$EVENT'
    
    candidates = []
    for d in sorted(os.listdir(assets_dir)):
        theme_dir = os.path.join(assets_dir, d)
        manifest = os.path.join(theme_dir, 'manifest.json')
        if not os.path.isfile(manifest):
            continue
        # Filter by theme: 'mix' uses all, otherwise match directory name
        if theme != 'mix' and d != theme:
            continue
        try:
            m = json.load(open(manifest))
            for f in m.get(event, []):
                path = os.path.join(theme_dir, f)
                if os.path.exists(path):
                    candidates.append(path)
        except (json.JSONDecodeError, IOError):
            continue
    
    for c in candidates:
        print(c)
    " 2>/dev/null)
    
    [ -z "$CANDIDATES" ] && exit 0
    
    # Pick random candidate (compatible with bash 3.x on macOS)
    IFS=$'\n' read -r -d '' -a FILES <<< "$CANDIDATES" || true
    COUNT=${#FILES[@]}
    [ "$COUNT" -eq 0 ] && exit 0
    FILE="${FILES[$((RANDOM % COUNT))]}"
    
    [ -z "$FILE" ] && exit 0
    
    # Convert path for WSL players that need Windows paths
    PLAY_FILE="$FILE"
    if [ "$IS_WSL" = true ] && [ "$PLAYER" = "ffplay.exe" -o "$PLAYER" = "powershell.exe" ]; then
      PLAY_FILE=$(wslpath -w "$FILE" 2>/dev/null || echo "$FILE")
    fi
    
    # Play with volume control (cross-platform)
    case "$PLAYER" in
      afplay)
        VOL=$(printf '%.2f' "$(echo "$SOUND_VOLUME / 100" | bc -l)")
        afplay -v "$VOL" "$FILE" &
        ;;
      paplay)
        PA_VOL=$((SOUND_VOLUME * 65536 / 100))
        paplay --volume="$PA_VOL" "$FILE" &
        ;;
      ffplay)
        ffplay -nodisp -autoexit -loglevel quiet -volume "$SOUND_VOLUME" "$FILE" &
        ;;
      ffplay.exe)
        ffplay.exe -nodisp -autoexit -loglevel quiet -volume "$SOUND_VOLUME" "$PLAY_FILE" &>/dev/null &
        ;;
      powershell.exe)
        powershell.exe -NoProfile -Command "\$w=New-Object -ComObject WMPlayer.OCX;\$w.settings.volume=$SOUND_VOLUME;\$w.URL='${PLAY_FILE}';\$null=\$w.controls;Start-Sleep 4;\$w.close()" &>/dev/null &
        ;;
      aplay)
        aplay -q "$FILE" &
        ;;
    esac
    exit 0
    

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 withsound-fx

Themed sound effects for Claude Code / Open Code — JARVIS, GLaDOS, Pikachu, and 9 more themes for your terminal

Get the whole plugin
Stats
45
Stars
3
Forks
Quiet
Maintenance
Python
Language
6mo ago
Last commit
7mo ago
Created

Repo: 6m1w/claude-sound-fx