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.
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
- Matches
Write|Editpython3 ${CLAUDE_PLUGIN_ROOT}/hooks/validate_track.pypython3 ${CLAUDE_PLUGIN_ROOT}/hooks/check_version_sync.py
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

