Skip to content
Automation
Hook

Hooks

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

From plugin
godotmaker
55941 skills7 agents15 hooks

Claude Code

$ npx -y skills add RandallLiuXin/GodotMaker --agent claude-code

Where it lives

  • hooks/__init__.pyGitHub
    Read the script
    """Hook modules used by GodotMaker runtime checks."""
    
  • hooks/check_asset_access.pyGitHub
    Read the script
    #!/usr/bin/env python3
    """PreToolUse hook: block active pipeline roles from reading asset images.
    
    The active /gm-* skill in the main session must delegate asset analysis to an
    analyst subagent. Regular coding-agent conversations with no current_role are
    not pipeline sessions and are allowed to read assets directly. Subagents are
    also allowed; only the main agent (empty agent_id) is blocked during a
    pipeline role.
    """
    import json
    import os
    import sys
    
    sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
    from metrics import get_current_role
    
    
    IMAGE_EXTENSIONS = {".png", ".jpg", ".jpeg", ".svg", ".webp", ".gif", ".bmp", ".tga"}
    
    
    def main():
        try:
            data = json.load(sys.stdin)
        except (json.JSONDecodeError, EOFError):
            sys.exit(0)
    
        if data.get("hook_event_name") != "PreToolUse":
            sys.exit(0)
        if data.get("tool_name") != "Read":
            sys.exit(0)
    
        agent_id = data.get("agent_id", "")
        if agent_id:
            sys.exit(0)
    
        if not get_current_role():
            sys.exit(0)
    
        tool_input = data.get("tool_input", {})
        file_path = tool_input.get("file_path", "")
        if not file_path:
            sys.exit(0)
    
        normalized = file_path.replace("\\", "/").lower()
    
        if "/assets/" not in normalized and not normalized.startswith("assets/"):
            sys.exit(0)
    
        _, ext = os.path.splitext(normalized)
        if ext not in IMAGE_EXTENSIONS:
            sys.exit(0)
    
        reason = (
            f"The active pipeline role cannot read image files in assets/ directly. "
            f"Dispatch an analyst subagent to analyze '{os.path.basename(file_path)}' instead. "
            f"See analyst-dispatch.md for the protocol."
        )
        print(json.dumps({"hookSpecificOutput": {
            "hookEventName": "PreToolUse",
            "permissionDecision": "deny",
            "permissionDecisionReason": reason,
        }}))
        sys.exit(0)
    
    
    if __name__ == "__main__":
        main()
    
  • hooks/check_clean_workspace.pyGitHub
    Read the script
    #!/usr/bin/env python3
    """Stop hook: warn the agent once per dirty episode at end of a SKILL.
    
    Block-once per dirty episode — agent gets reminded, can commit/clean/ignore.
    Clean state resets the flag so the next dirty episode gets its own reminder.
    Subagents skipped.
    """
    import json
    import os
    import subprocess
    import sys
    
    sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
    from metrics import get_current_role, state
    
    
    def main():
        try:
            data = json.load(sys.stdin)
        except (json.JSONDecodeError, EOFError):
            sys.exit(0)
        if data.get("agent_id"):
            sys.exit(0)
        role = get_current_role()
        if not role:
            sys.exit(0)
    
        # Role change between Stop events resets the reminder flag, so the new
        # role gets its own first-dirty-stop reminder instead of inheriting the
        # previous role's "already reminded" state.
        if state.get("dirty_reminder_last_role", None) != role:
            state.put("dirty_reminded", False)
            state.put("dirty_reminder_last_role", role)
    
        r = subprocess.run(
            ["git", "status", "--porcelain"],
            capture_output=True, text=True
        )
        if r.returncode != 0:
            sys.exit(0)
        output = r.stdout.strip()
        if not output:
            state.put("dirty_reminded", False)
            sys.exit(0)
        if state.get("dirty_reminded", False):
            sys.exit(0)
    
        state.put("dirty_reminded", True)
        snippet = output[:800] + ("\n... (truncated)" if len(output) > 800 else "")
        reminder = (
            f"Working tree dirty at end of '{role}':\n{snippet}\n\n"
            f'Run `git add -A && git commit -m "..."`, remove unintended files, '
            f"or exit again to skip."
        )
        print(json.dumps({"decision": "block", "reason": reminder}))
        sys.exit(0)
    
    
    if __name__ == "__main__":
        main()
    
  • hooks/check_completion.pyGitHub
    Read the script
    #!/usr/bin/env python3
    """Stop hook: verify dispatcher diligence in worker-dispatching roles.
    
    Only enforced when current_role is "build" or "fixgap" — these are the roles
    that dispatch workers and require verifier + reviewer rounds. Other roles
    (scaffold, gdd, asset, verify, evaluate, accept, finalize) self-enforce via
    their SKILL.md Resume Check and skip this hook.
    
    Diligence rule: if workers were dispatched in this session, both verifier
    and reviewer must have run too (per gm-build/gm-fixgap Hard Rule 6).
    
    Anti-deadloop: if blocked BLOCK_LIMIT times in the same session, allow with
    a warning rather than blocking again.
    
    Only blocks the main agent (the gm-* skill), not subagents.
    """
    import json
    import os
    import sys
    
    sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
    from metrics import (
        record_event, read_current_events, EventType, state, event_has_role,
        get_current_role, WORKER_DISPATCH_ROLES,
    )
    
    BLOCK_LIMIT = 5
    
    LIFECYCLE_EVENTS = (EventType.SUBAGENT_START, EventType.SUBAGENT_STOP)
    
    
    def check_diligence(events: list[dict], require_reviewer: bool = True) -> list[str]:
        """Check that workers had verifiers (and reviewers) dispatched."""
        if not events:
            return []
    
        worker_count = 0
        verifier_seen = False
        reviewer_seen = False
        for e in events:
            ev = e.get("event")
            if ev == EventType.SUBAGENT_START and event_has_role(e, "worker"):
                worker_count += 1
            if ev in LIFECYCLE_EVENTS:
                if not verifier_seen and event_has_role(e, "verifier"):
                    verifier_seen = True
                if not reviewer_seen and event_has_role(e, "reviewer"):
                    reviewer_seen = True
    
        if worker_count == 0:
            return []
    
        issues = []
        n = worker_count
        if not verifier_seen:
            issues.append(
                f"Dispatched {n} workers but 0 verifiers. "
                "Dispatch a verifier (subagent_type: 'verifier') to confirm "
                "code/tests are real and the build passes."
            )
        if require_reviewer and not reviewer_seen:
            issues.append(
                f"Dispatched {n} workers but 0 reviewers. "
                "Dispatch a reviewer (subagent_type: 'reviewer') to check "
                "code quality and ECS patterns."
            )
        return issues
    
    
    def get_diligence_failure(role: str, events: list[dict]) -> dict | None:
        """Return a structured completion failure without producing side effects.
    
        This is the reusable pre-append decision. Callers own any subsequent
        metrics, state, archive, or stage-log mutations.
        """
        if role not in WORKER_DISPATCH_ROLES:
            return None
    
        issues = check_diligence(events, require_reviewer=True)
        if not issues:
            return None
    
        reason = (
            f"Cannot finish '{role}' role — diligence issues:\n"
            + "\n".join(f"  - {line}" for line in issues)
        )
        return {
            "decision": "block",
            "role": role,
            "issues": issues,
            "reason": reason,
        }
    
    
    def main():
        try:
            data = json.load(sys.stdin)
        except (json.JSONDecodeError, EOFError):
            sys.exit(0)
    
        if data.get("agent_id", ""):
            sys.exit(0)
    
        role = get_current_role()
        if role not in WORKER_DISPATCH_ROLES:
            record_event(EventType.GATE_CHECK, gate="completion",
                         result="skip", role=role)
            sys.exit(0)
    
        block_count = state.get("stop_block_count", 0)
        if block_count >= BLOCK_LIMIT:
            record_event(EventType.GATE_CHECK, gate="completion",
                         result="force_allow", role=role,
                         reason=f"Blocked {block_count} times, allowing to prevent deadloop")
            warning = (
                f"Force-allowing completion after {block_count} failed attempts. "
                "You MUST tell the user that diligence checks did not fully pass. "
                "List the unresolved issues so the user can decide whether to "
                "re-run /gm-build or accept as-is."
            )
            print(json.dumps({"decision": "allow", "reason": warning}), file=sys.stderr)
            sys.exit(0)
    
        events = read_current_events()
        failure = get_diligence_failure(role, events)
    
        if failure:
            state.increment("stop_block_count")
            record_event(EventType.GATE_CHECK, gate="completion",
                         result="fail", role=role, issues=failure["issues"][:5])
            print(json.dumps({
                "decision": failure["decision"],
                "reason": failure["reason"],
            }))
            sys.exit(0)
    
        record_event(EventType.GATE_CHECK, gate="completion", result="pass", role=role)
    
    
    if __name__ == "__main__":
        main()
    
  • hooks/check_file_permissions.pyGitHub
    Read the script
    #!/usr/bin/env python3
    """PreToolUse hook: enforce file write permissions per pipeline role.
    
    Reads .godotmaker/current_role and applies the role's write rules. See the
    gm-*/SKILL.md files for the canonical per-role rules; this hook enforces
    them. When no role is set, no /gm-* pipeline role is active, so regular
    coding-agent conversations are allowed to write normally.
    """
    import json
    import os
    import sys
    
    sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
    from metrics import record_event, EventType, get_current_role, WORKER_DISPATCH_ROLES
    
    GAME_CODE_EXTENSIONS = {".gd", ".tscn", ".tres"}
    # Project-root planning artifacts — subagents may NOT modify these unless
    # their agent_type is in PLANNING_WRITER_AGENT_TYPES. Includes the four
    # 1c decomposer outputs (PLAN/STRUCTURE/DESIGN/ASSETS + SCENES/TOC), the
    # tag-iteration roadmap (ROADMAP.md, owned by /gm-gdd), and GAP.md
    # (owned by /gm-fixgap's lead, not subagents).
    PLANNING_DOCS = {"plan.md", "structure.md", "design.md", "assets.md", "gap.md",
                     "scenes.md", "toc.md", "roadmap.md"}
    # project.godot is the engine config and changes the whole game. Subagents
    # may not edit it unless their agent_type is in PLANNING_WRITER_AGENT_TYPES.
    PROJECT_GODOT = "project.godot"
    E2E_DIR_PREFIX = "e2e/"
    ASSETS_DIR_PREFIX = "assets/"
    REFERENCES_DIR_PREFIX = "references/"
    GODOTMAKER_DIR = ".godotmaker/"
    # Subagent types whose entire purpose is writing planning docs — exempt
    # from the general subagent block on PLANNING_DOCS and PROJECT_GODOT.
    PLANNING_WRITER_AGENT_TYPES = {"decomposer"}
    # Per-role narrow write allow-lists under .godotmaker/. Each role needs
    # current_role + stage.jsonl for bookkeeping; evaluate / verify also write
    # their structured verdict; rescue is diagnostic-only (chat output only),
    # so it gets ONLY the bookkeeping pair — anything else attempted is a SKILL
    # violation worth blocking.
    EVAL_ALLOWED_GM_FILES = {".godotmaker/evaluation.json",
                              ".godotmaker/stage.jsonl",
                              ".godotmaker/current_role"}
    # Evaluator-owned directory: per-subject design rule check request, raw
    # findings, and graded result.
    DESIGN_CHECKS_DIR = ".godotmaker/design-checks/"
    VERIFY_ALLOWED_GM_FILES = {".godotmaker/stage.jsonl",
                                ".godotmaker/current_role",
                                ".godotmaker/verify_report.json"}
    RESCUE_ALLOWED_GM_FILES = {".godotmaker/stage.jsonl",
                                ".godotmaker/current_role"}
    
    
    def _is_e2e_path(path_lower: str) -> bool:
        return path_lower.startswith(E2E_DIR_PREFIX) or f"/{E2E_DIR_PREFIX}" in path_lower
    
    
    def _is_assets_path(path_lower: str) -> bool:
        return path_lower.startswith(ASSETS_DIR_PREFIX) or f"/{ASSETS_DIR_PREFIX}" in path_lower
    
    
    def _is_references_path(path_lower: str) -> bool:
        return path_lower.startswith(REFERENCES_DIR_PREFIX) or f"/{REFERENCES_DIR_PREFIX}" in path_lower
    
    
    def _is_godotmaker_path(path_lower: str) -> bool:
        return path_lower.startswith(GODOTMAKER_DIR) or f"/{GODOTMAKER_DIR}" in path_lower
    
    
    def _is_design_check_path(path_lower: str) -> bool:
        """True for the evaluator-owned design rule check artefacts.
    
        `/gm-evaluate` writes one request, findings, and graded result per subject
        here so the chain from a DESIGN.md rule to its verdict stays inspectable.
        """
        return (
            path_lower.startswith(DESIGN_CHECKS_DIR)
            or f"/{DESIGN_CHECKS_DIR}" in path_lower
        )
    
    
    def _is_asset_generation_path(path_lower: str) -> bool:
        return (
            path_lower.startswith(".godotmaker/asset-generation/")
            or "/.godotmaker/asset-generation/" in path_lower
        )
    
    
    def _is_canonical_asset_request_path(path_lower: str) -> bool:
        """True for a top-level canonical asset-generation request.
    
        `/gm-asset` owns these immutable request files. Producers may still write
        resolved/internal request artifacts below `work/` or `plans/`, except for
        the manager-owned UI-kit pre-dispatch planning set checked separately.
        """
        marker = ".godotmaker/asset-generation/"
        normalized = path_lower.replace("\\", "/")
        marker_index = normalized.find(marker)
        if marker_index < 0:
            return False
        relative = normalized[marker_index + len(marker):]
        return "/" not in relative and relative.endswith("-request.json")
    
    
    def _is_manager_owned_asset_planning_path(path_lower: str) -> bool:
        """True for the manager-owned UI-kit pre-dispatch planning set."""
        marker = ".godotmaker/asset-generation/work/"
        normalized = path_lower.replace("\\", "/")
        marker_index = normalized.find(marker)
        if marker_index < 0:
            return False
        relative = normalized[marker_index + len(marker):]
        parts = relative.split("/")
        return len(parts) == 2 and parts[1] in {
            "planning-request.json",
            "theme_plan.json",
            "source_sheet_plan.json",
            "stylebox_plan.json",
            "theme_recipe.json",
        }
    
    
    def _matches_allowed_gm(path_lower: str, allowed: set[str]) -> bool:
        """True if path ends with one of the allowed `.godotmaker/<file>` entries."""
        return any(path_lower.endswith(p) for p in allowed)
    
    
    def _is_project_root_assets_md(path_lower: str) -> bool:
        """True iff path_lower resolves to the project-root ASSETS.md.
    
        The hook runs with cwd = project root, so abspath() of a relative
        input is anchored to that root. Accepts both bare `"assets.md"` and
        any absolute path that resolves to `<cwd>/ASSETS.md`. Subdirectory
        variants (`subdir/ASSETS.md`) and absolute paths to a different
        project's ASSETS.md are rejected — the asset role's contract and
        the deny message in `_check_main` are explicitly project-root only.
    
        Uses realpath (not abspath) because macOS routes /var/folders/...
        through a /private/var/folders/... symlink — abspath leaves the
        input untouched but cwd-derived paths often arrive already resolved,
        so the two sides drift and a legitimately-rooted ASSETS.md gets
        rejected
  • hooks/check_stage_prerequisites.pyGitHub
    Read the script
    #!/usr/bin/env python3
    """PreToolUse hook (Agent/Task tool): verify role prerequisites before dispatch.
    
    Only enforces for roles that drive worker orchestration:
      - build → requires gdd completed + scaffold artifacts present
      - fixgap → requires evaluate completed + evaluation.json present
    
    Scaffold artifacts are checked on disk because gm-finalize truncates
    stage.jsonl at every tag boundary, so the scaffold event is no longer
    in stage.jsonl after the first finalize.
    
    Other dispatching roles (asset → analyst) self-validate via their SKILL.md
    Resume Check; their preconditions don't match this hook's stage-schema model.
    """
    import json
    import os
    import sys
    
    sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
    from metrics import (
        record_event, EventType,
        get_current_role, get_completed_roles,
        load_stage_schemas, WORKER_DISPATCH_ROLES,
    )
    
    
    PREREQ_ROLE = {
        "build": "gdd",
        "fixgap": "evaluate",
    }
    
    # Sanity check: PREREQ_ROLE must cover exactly the worker-dispatching roles.
    # Use an explicit raise instead of assert so the check survives `python -O`.
    if frozenset(PREREQ_ROLE) != WORKER_DISPATCH_ROLES:
        raise RuntimeError(
            f"PREREQ_ROLE keys {sorted(PREREQ_ROLE)} must equal "
            f"WORKER_DISPATCH_ROLES {sorted(WORKER_DISPATCH_ROLES)}"
        )
    
    # Roles that need scaffold artifacts on disk (lifetime-once, not in stage.jsonl
    # after the first tag's finalize truncates the log).
    SCAFFOLD_REQUIRED = frozenset({"build"})
    
    
    def main():
        try:
            data = json.load(sys.stdin)
        except (json.JSONDecodeError, EOFError):
            sys.exit(0)
    
        if data.get("tool_name") not in {"Agent", "Task"}:
            sys.exit(0)
    
        # Only check main agent (the gm-* skill orchestrating dispatch)
        if data.get("agent_id", ""):
            sys.exit(0)
    
        role = get_current_role()
        prereq = PREREQ_ROLE.get(role)
        if not prereq:
            sys.exit(0)  # No worker-dispatch role active, nothing to enforce
    
        completed = get_completed_roles()
        issues = []
    
        if role in SCAFFOLD_REQUIRED and not os.path.isfile("project.godot"):
            issues.append("project.godot not found — run /gm-scaffold first")
    
        if prereq not in completed:
            issues.append(f"Role '{prereq}' has not completed yet — run /gm-{prereq} first")
    
        schemas = load_stage_schemas()
        if schemas:
            prereq_schema = schemas.get(prereq, {})
            # Inline existence check here (not validate_schema_files) to keep the
            # role-aware "{prereq} output missing: X" message style.
            for filepath in prereq_schema.get("files", []):
                if not os.path.exists(filepath):
                    issues.append(f"{prereq} output missing: {filepath}")
    
        if issues:
            reason = (
                f"Cannot dispatch worker — '{role}' role prerequisites missing:\n"
                + "\n".join(f"  - {m}" for m in issues)
            )
            record_event(EventType.HOOK_BLOCK, hook="check_stage_prerequisites",
                         role=role, missing=issues)
            print(json.dumps({"hookSpecificOutput": {
                "hookEventName": "PreToolUse",
                "permissionDecision": "deny",
                "permissionDecisionReason": reason,
            }}))
            sys.exit(0)
    
        record_event(EventType.HOOK_ALLOW, hook="check_stage_prerequisites", role=role)
    
    
    if __name__ == "__main__":
        main()
    
  • hooks/check_worker_report.pyGitHub
  • hooks/completion_gate.pyGitHub
  • hooks/log_agent_tool.pyGitHub
  • hooks/log_compaction.pyGitHub
  • hooks/log_subagent.pyGitHub
  • hooks/on_subagent_stop.pyGitHub
  • hooks/playability_contract.pyGitHub
  • hooks/session_start.pyGitHub
  • hooks/stage_reminder.pyGitHub

All 15 scripts are listed above. The source is inlined for 6 of them, starting with whatever hooks.json actually runs. See all of them in the repo.

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.

@randallliuxin
Ships withgodotmaker

Autonomous text-to-game pipeline for Godot, powered by Claude Code,Codex,Opencode

55941 skills7 agents
Get the whole plugin

Stars on GitHub

Momentum

stars in total
559
Star history of RandallLiuXin/GodotMaker on GitHub, a chart by star-history.com

More plugins that ship hooks.

n8n-mcp-skills

n8n-mcp-skills

Expert Claude Code skills for building flawless n8n workflows using the n8n-mcp MCP server.

15 skills3 hooks6.6k
@czlonkowski@czlonkowski
Install Plugin
claude-scholar

claude-scholar

A research assistant plugin for Claude Code that supports literature review, paper writing, citation verification, Zotero and Obsidian integration, and code review and debugging for academic and software work.

45 skills+35.9k
@galaxy-dawn@galaxy-dawn
Install Plugin
octo

octo

Runs multi-AI workflows in Claude Code that split research, requirements scoping, implementation and review across several external AI providers, such as Codex, Gemini-style CLIs, Copilot, Ollama and OpenRouter, to cover each model's blind spots.

71 skills+34.4k
@nyldn@nyldn
Install Plugin
ai-maestro

ai-maestro

Orchestrates multiple Claude Code agents across terminals and machines, giving them persistent memory and agent-to-agent messaging so you no longer have to relay messages between them by hand.

33 hooks1 MCP841
@23blocks-os@23blocks-os
Install Plugin
sutando

sutando

A personal AI assistant with voice, screen and meeting support that learns your patterns and can update its own code. It runs across several Macs and can talk with other people's assistants.

79 skills18 hooks401
@sonichi@sonichi
Install Plugin
nextboard-hardware-solution

nextboard-hardware-sol…

Helps hardware teams turn product requirements into a reviewable, buildable PCB design plan. Covers embedded architecture, MCU or SoC selection, power tree, interfaces, sensors, connectivity, and BOM risk.

1 skill+2395
@leokemp223@leokemp223
Install Plugin
claude-prompts

claude-prompts

MCP server for reusable prompt templates, multi-step workflow chains, and quality gates. Compose agentic workflows with an operator syntax; export as native skills to Claude Code, Cursor, OpenCode, and Gemini CLI.

5 hooks1 MCP191
@minipuft@minipuft
Install Plugin
crypto-trading-desk

crypto-trading-desk

A Claude Code plugin for crypto trading with skills to run multi-agent market analysis, track a portfolio and open trades, close trades with post-mortem review, and monitor stop-loss and take-profit levels on a schedule.

8 skills+2107
@hugoguerrap@hugoguerrap
Install Plugin

Questions about the hooks in godotmaker.

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