Skip to content
Content
Hook

Hooks

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

From plugin
bitwize-music
50553 skills1 hook1 MCP
Install
> /plugin marketplace add bitwize-music-studio/claude-ai-music-skills
> /plugin install bitwize-music@bitwize-music

Ships with bitwize-music. Installing the plugin gets these hooks.

What fires, and when

PostToolUse

  • MatchesWrite|Editpython3 ${CLAUDE_PLUGIN_ROOT}/hooks/validate_track.pypython3 ${CLAUDE_PLUGIN_ROOT}/hooks/check_version_sync.py
Read hooks/hooks.json

In the plugin's words

How bitwize-music describes its own hook set.

Bitwize Music plugin hooks for quality enforcement

Where it lives

  • hooks/check_version_sync.pyRunsGitHub
    Read the script
    #!/usr/bin/env python3
    """PostToolUse hook: Check plugin.json and marketplace.json versions stay in sync.
    
    Only activates when editing plugin.json or marketplace.json.
    """
    import json
    import os
    import subprocess
    import sys
    
    MANIFEST_FILES = {"plugin.json", "marketplace.json"}
    
    
    def is_manifest_file(file_path: str) -> bool:
        return os.path.basename(file_path) in MANIFEST_FILES and ".claude-plugin" in file_path
    
    
    def check_sync(data: dict) -> list[str]:
        tool_input = data.get("tool_input", {})
        file_path = tool_input.get("file_path", "")
    
        if not is_manifest_file(file_path):
            return []
    
        plugin_dir = os.path.dirname(file_path)
        plugin_path = os.path.join(plugin_dir, "plugin.json")
        marketplace_path = os.path.join(plugin_dir, "marketplace.json")
    
        if not os.path.exists(plugin_path) or not os.path.exists(marketplace_path):
            return []
    
        try:
            with open(plugin_path, encoding="utf-8") as f:
                plugin_data = json.load(f)
            with open(marketplace_path, encoding="utf-8") as f:
                marketplace_data = json.load(f)
        except (json.JSONDecodeError, OSError, UnicodeDecodeError):
            return []
    
        plugin_version = plugin_data.get("version", "")
        marketplace_version = ""
        plugins = marketplace_data.get("plugins", [])
        if plugins:
            marketplace_version = plugins[0].get("version", "")
    
        if plugin_version and marketplace_version and plugin_version != marketplace_version:
            # If the other file is also modified in the working tree, this is
            # a mid-edit pair (user updating both files sequentially). Skip.
            try:
                result = subprocess.run(
                    ["git", "diff", "--name-only"],
                    capture_output=True, text=True, timeout=5,
                    cwd=plugin_dir,
                )
                modified = set(result.stdout.strip().splitlines())
                other_file = "marketplace.json" if file_path.endswith("plugin.json") else "plugin.json"
                if other_file in modified or os.path.join(".claude-plugin", other_file) in modified:
                    return []
            except (subprocess.TimeoutExpired, OSError):
                pass
    
            return [
                f"Version mismatch: plugin.json has '{plugin_version}' "
                f"but marketplace.json has '{marketplace_version}'. "
                f"These must stay in sync."
            ]
    
        return []
    
    
    def main():
        try:
            data = json.load(sys.stdin)
        except (json.JSONDecodeError, EOFError):
            sys.exit(0)
    
        issues = check_sync(data)
        if issues:
            msg = "Version sync check failed:\n" + "\n".join(f"  - {i}" for i in issues)
            print(msg, file=sys.stderr)
            sys.exit(2)
    
        sys.exit(0)
    
    
    if __name__ == "__main__":
        main()
    
  • hooks/install.shGitHub
    Read the script
    #!/bin/bash
    # Install git hooks for bitwize-music plugin
    
    set -e
    
    SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
    REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
    HOOKS_DIR="$REPO_ROOT/.git/hooks"
    
    echo "Installing git hooks..."
    
    # Install pre-commit hook
    if [ -f "$SCRIPT_DIR/pre-commit" ]; then
        cp "$SCRIPT_DIR/pre-commit" "$HOOKS_DIR/pre-commit"
        chmod +x "$HOOKS_DIR/pre-commit"
        echo "✓ Installed pre-commit hook"
    else
        echo "✗ pre-commit hook not found"
        exit 1
    fi
    
    echo ""
    echo "Git hooks installed successfully!"
    echo ""
    echo "The pre-commit hook requires:"
    echo "  - ruff (pip install ruff)"
    echo "  - pytest (pip install pytest)"
    echo "  - bandit (pip install bandit)"
    echo "  - pip-audit (pip install pip-audit)"
    echo ""
    echo "Install all at once:"
    echo "  pip install ruff pytest bandit pip-audit"
    
  • hooks/validate_track.pyRunsGitHub
    Read the script
    #!/usr/bin/env python3
    """PostToolUse hook: Validate track file YAML frontmatter after Write/Edit.
    
    Only activates for files matching */tracks/*.md pattern.
    Checks required frontmatter fields and valid status values.
    """
    from __future__ import annotations
    
    import json
    import re
    import sys
    
    REQUIRED_FIELDS = ["title", "track_number", "status"]
    VALID_STATUSES = [
        "Not Started",
        "Sources Pending",
        "Sources Verified",
        "In Progress",
        "Generated",
        "Final",
    ]
    
    
    def is_track_file(file_path: str) -> bool:
        """True for ``.md`` files living in a directory segment named ``tracks``.
    
        Claude Code passes the platform's *native* path, so on Windows this arrives
        backslash-separated (``...\\tracks\\01-opener.md``) and mixed separators
        (``C:/foo\\tracks/01-x.md``) are possible too. Backslashes are folded to
        forward slashes before splitting rather than going through ``pathlib``:
        ``PurePosixPath`` would not split backslashes at all, ``PureWindowsPath``
        behaves identically to this but costs an import on a hook that runs on
        every Write/Edit. The trade-off is that a POSIX file whose *name* legally
        contains a backslash could be split at it — vanishingly rare, and the
        failure mode is a harmless extra frontmatter check.
    
        Matching whole segments (rather than the old ``"/tracks/" in path``
        substring test) also keeps ``soundtracks/``, ``tracks-old/`` and
        ``tracksnotadir/`` from matching, and additionally handles relative paths
        such as ``tracks/01-x.md``, which the substring form rejected.
        """
        if not file_path.endswith(".md"):
            return False
        # Exclude the final component: that is the filename, not a directory.
        return "tracks" in file_path.replace("\\", "/").split("/")[:-1]
    
    
    def extract_frontmatter(content: str) -> dict | None:
        match = re.match(r"^---\s*\n(.*?)\n---", content, re.DOTALL)
        if not match:
            return None
        fm = {}
        for line in match.group(1).split("\n"):
            if ":" in line:
                key, _, value = line.partition(":")
                fm[key.strip()] = value.strip().strip('"').strip("'")
        return fm
    
    
    def get_file_content(data: dict) -> str | None:
        tool_input = data.get("tool_input", {})
        # Write tool provides full content
        if "content" in tool_input:
            return tool_input["content"]
        return None
    
    
    def validate(data: dict) -> list[str]:
        tool_input = data.get("tool_input", {})
        file_path = tool_input.get("file_path", "")
    
        if not is_track_file(file_path):
            return []
    
        content = get_file_content(data)
        if content is None:
            # Edit tool — can't validate full frontmatter from partial edit
            return []
    
        fm = extract_frontmatter(content)
        if fm is None:
            return ["Track file is missing YAML frontmatter (--- block)."]
    
        issues = []
        for field in REQUIRED_FIELDS:
            if field not in fm or not fm[field]:
                issues.append(f"Missing required frontmatter field: {field}")
    
        status = fm.get("status", "")
        if status and status not in VALID_STATUSES:
            issues.append(
                f"Invalid status '{status}'. Must be one of: {', '.join(VALID_STATUSES)}"
            )
    
        return issues
    
    
    def main():
        try:
            data = json.load(sys.stdin)
        except (json.JSONDecodeError, EOFError):
            sys.exit(0)
    
        # Well-formed JSON that is not an object (a list, null, a number, a string)
        # has no `tool_input` to inspect. There is nothing to validate, and this
        # hook must never break the user's session over an unexpected payload.
        if not isinstance(data, dict):
            sys.exit(0)
    
        issues = validate(data)
        if issues:
            msg = "Track frontmatter validation failed:\n" + "\n".join(f"  - {i}" for i in issues)
            print(msg, file=sys.stderr)
            sys.exit(2)
    
        sys.exit(0)
    
    
    if __name__ == "__main__":
        main()
    

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 withbitwize-music

I love music but never learned an instrument. AI became the creative outlet that was always out of reach. This project started as a way to go deep on Claude Code plugin architecture, agentic workflows, multi-model orchestration, and MCP tooling.

Get the whole plugin
Stats
513
Stars
120
Forks
Active
Maintenance
Python
Language
CC0-1.0
License
15h ago
Last commit
8mo ago
Created

Repo: bitwize-music-studio/claude-ai-music-skills