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
64853 skills1 hook1 MCP

Claude Code

> /plugin marketplace add bitwize-music-studio/claude-ai-music-skills
> /plugin install bitwize-music@bitwize-music

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: keep the Claude and Codex plugin versions in sync.
    
    The Claude manifest and marketplace entry are the canonical pair.  When the
    Codex manifest exists, its version must match the Claude plugin manifest too,
    but only for this plugin: the hook ships to every user and runs on every
    Write/Edit, and other dual-host plugins may version their Codex adapter
    separately.  Malformed or partially-written manifests are ignored so an
    editor's transient state does not turn this advisory hook into a hard failure.
    """
    
    import json
    import os
    import subprocess
    import sys
    from pathlib import Path
    
    MANIFEST_FILES = {"plugin.json", "marketplace.json"}
    PLUGIN_DIRS = {".claude-plugin", ".codex-plugin"}
    # The Codex-equals-Claude version rule is this plugin's release policy, not a
    # general one; see the module docstring.
    CODEX_SYNC_PLUGIN_NAME = "bitwize-music"
    
    
    def _manifest_context(file_path: str):
        """Return ``(repository_root, manifest_dir, path)`` for a manifest path."""
        if not isinstance(file_path, str) or not file_path:
            return None
    
        path = Path(file_path)
        if path.name not in MANIFEST_FILES:
            return None
    
        for parent in (path.parent, *path.parents):
            if parent.name in PLUGIN_DIRS:
                return parent.parent, parent.name, path
        return None
    
    
    def is_manifest_file(file_path: str) -> bool:
        return _manifest_context(file_path) is not None
    
    
    def _read_json(path: Path):
        try:
            with path.open(encoding="utf-8") as f:
                return json.load(f)
        except (json.JSONDecodeError, OSError, UnicodeDecodeError):
            return None
    
    
    def _plugin_version(data) -> str:
        return data.get("version", "") if isinstance(data, dict) else ""
    
    
    def _marketplace_version(data) -> str:
        if not isinstance(data, dict):
            return ""
        plugins = data.get("plugins", [])
        if not isinstance(plugins, list) or not plugins or not isinstance(plugins[0], dict):
            return ""
        return plugins[0].get("version", "")
    
    
    def _version_pairs(root: Path):
        """Return version pairs that can be checked from this repository root."""
        claude_plugin_path = root / ".claude-plugin" / "plugin.json"
        marketplace_path = root / ".claude-plugin" / "marketplace.json"
    
        claude_plugin = _read_json(claude_plugin_path)
        if not isinstance(claude_plugin, dict):
            return []
    
        pairs = []
        claude_version = _plugin_version(claude_plugin)
        # A missing or half-written marketplace.json drops only its own pair; it
        # must not also skip the independent Claude-vs-Codex comparison below.
        marketplace_version = _marketplace_version(_read_json(marketplace_path))
        if claude_version and marketplace_version:
            pairs.append(
                (
                    claude_plugin_path,
                    "plugin.json",
                    claude_version,
                    marketplace_path,
                    "marketplace.json",
                    marketplace_version,
                )
            )
    
        codex_plugin_path = root / ".codex-plugin" / "plugin.json"
        if claude_plugin.get("name") == CODEX_SYNC_PLUGIN_NAME and codex_plugin_path.exists():
            codex_version = _plugin_version(_read_json(codex_plugin_path))
            if claude_version and codex_version:
                pairs.append(
                    (
                        claude_plugin_path,
                        ".claude-plugin/plugin.json",
                        claude_version,
                        codex_plugin_path,
                        ".codex-plugin/plugin.json",
                        codex_version,
                    )
                )
    
        return pairs
    
    
    def _same_path(left: Path, right: Path) -> bool:
        return os.path.normcase(os.path.abspath(str(left))) == os.path.normcase(
            os.path.abspath(str(right))
        )
    
    
    def _modified_files(root: Path) -> set[str]:
        # This runs in every user's session, inside whatever repo holds the edited
        # manifest. Repo-local config such as core.fsmonitor can make git run
        # commands, and safe.bareRepository=explicit stops git from discovering a
        # bare repository planted inside a project. Command-line -c settings take
        # precedence over repo config and count as protected configuration.
        try:
            result = subprocess.run(
                [
                    "git",
                    "-c",
                    "core.fsmonitor=false",
                    "-c",
                    "safe.bareRepository=explicit",
                    "diff",
                    "--name-only",
                ],
                capture_output=True,
                text=True,
                timeout=5,
                cwd=str(root),
            )
        except (subprocess.TimeoutExpired, OSError):
            return set()
    
        return {
            line.strip().replace("\\", "/")
            for line in result.stdout.splitlines()
            if line.strip()
        }
    
    
    def _is_modified(path: Path, root: Path, modified: set[str]) -> bool:
        try:
            relative = os.path.relpath(path, root).replace(os.sep, "/")
        except ValueError:
            relative = ""
        absolute = str(path).replace(os.sep, "/")
        return relative in modified or absolute in modified
    
    
    def check_sync(data: dict) -> list[str]:
        tool_input = data.get("tool_input", {})
        file_path = tool_input.get("file_path", "")
        context = _manifest_context(file_path)
        if context is None:
            return []
    
        root, _, _ = context
        issues = []
        modified = None
        for left_path, left_label, left_version, right_path, right_label, right_version in _version_pairs(root):
            if left_version == right_version:
                continue
    
            # If the counterpart is also modified, this is likely a sequential
            # edit of a version pair. Defer until the second file is saved.
            counterpart = None
            if _same_path(Path(file_path), left_path):
                counterpart = right_path
            elif _same_path(Path(file_path), right_path):
                counterpart = left_path
            if counterpart is not None:
                if modified is None:
                    modified = _mo
  • 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.

@bitwize-music-studio
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.

64853 skills1 MCP
Get the whole plugin

Stars on GitHub

Momentum

stars in total
648
Star history of bitwize-music-studio/claude-ai-music-skills on GitHub, a chart by star-history.com

More plugins that ship hooks.

watch

watch

Lets Claude watch a video from a URL or local file by pulling timestamped frames and a transcript. Works in Claude Code, and the watch skill downloads with yt-dlp and extracts frames with ffmpeg.

1 skill1 hook20k
@bradautomates@bradautomates
Install Plugin
meigen-ai-design-mcp

meigen-ai-design-mcp

Gives Claude Code users access to MeiGen for discovering inspiration and creating images and videos with AI models. It also offers skills for product photoshoot planning, social video thumbnails, and visual creative direction.

3 skills+41.8k
@jau123@jau123
Install Plugin
capcut-cli

capcut-cli

Edits CapCut and JianYing video projects from the terminal or an LLM agent, covering subtitles, timing, speed, volume, templates, animations, and cutting long videos into shorts. The result opens in CapCut with every track still editable.

1 skill1 hook1.1k
@renezander030@renezander030
Install Plugin
wind-comic

wind-comic

Turns a single line of story text into a finished short drama, covering the script, storyboards, character-consistent video and voiceover. Works with the AI model providers you already use, so you skip assembling each step by hand.

7 skills11 hooks714
@chrischen667788@chrischen667788
Install Plugin
master-skill

master-skill

A skill that builds AI teaching personas of specific Buddhist masters, grounded in Buddhist scripture texts and tested for fidelity to their teachings. It includes ethics gates for boundaries on what the persona will say.

21 skills1 hook495
@xr843@xr843
Install Plugin
claude-skills-journalism

claude-skills-journali…

Gives journalists and researchers ready-made guidance for fact-checking, source verification, interview prep, story pitches and data journalism, plus accessibility audits for news sites. Works in Claude Code and Codex.

72 skills+3451
@jamditis@jamditis
Install Plugin
showtime

showtime

A local video studio for your coding agent. Describe a video in one sentence: your agent directs, your machine renders. Claude Code, Codex, Cursor, Devin and more. No API keys, no uploads.

1 skill+2310
@faviovazquez@faviovazquez
Install Plugin
claude-canvas

claude-canvas

Creates and lays out Obsidian Canvas files as presentations, flowcharts, mind maps, mood boards, and knowledge graphs. It can also fill canvases with images, notes, and diagrams, generate content with AI, and export the results to PNG, SVG, or PDF.

8 skills+3302
@agricidaniel@agricidaniel
Install Plugin

Questions about the hooks in bitwize-music.

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