Skip to content
Data
Hook

Hooks

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

From plugin
nutmeg
5611 skills3 agents3 hooks1 MCP

Claude Code

> /plugin marketplace add withqwerty/nutmeg
> /plugin install nutmeg@nutmeg

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.

  • sh "${CLAUDE_PLUGIN_ROOT}/hooks/session-start.sh"

PreToolUse

  • MatchesBashsh "${CLAUDE_PLUGIN_ROOT}/hooks/run-hook.sh" gate_hook.py

Stop

  • sh "${CLAUDE_PLUGIN_ROOT}/hooks/run-hook.sh" stop_check.py
Read hooks/hooks.json

In the plugin's words

How nutmeg describes its own hook set.

Adds nutmeg's core football-data and research-project rules to each session. Inside an active research project, shows a gate card before runs and checks outputs against the claim ledger before a step ends.

Where it lives

  • hooks/gate_hook.pyGitHub
    Read the script
    """PreToolUse hook: show a gate card before runs inside a research project.
    
    Called through run-hook.sh, which has already found an active project. Prints
    a permission decision as JSON, or nothing to leave the command to Claude
    Code's normal permission flow.
    """
    import json
    import os
    import sys
    from pathlib import Path
    
    sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "core"))
    
    from nutmeg_core import gate, project  # noqa: E402
    
    
    def main():
        try:
            data = json.load(sys.stdin)
        except (json.JSONDecodeError, ValueError):
            return 0
        if data.get("tool_name") != "Bash":
            return 0
        command = (data.get("tool_input") or {}).get("command") or ""
        cwd = data.get("cwd") or os.getcwd()
        repo = Path(os.environ.get("NUTMEG_REPO_ROOT") or cwd)
        active = project.active_project(repo)
        try:
            decision = gate.decide(command, repo, active, cwd)
        except Exception as exc:  # a broken gate must not wave a run through
            if active is None or gate.parse(command).kind == "other":
                return 0
            decision = gate.Decision("ask", f"nutmeg research gate failed ({exc}); read the command yourself before approving.")
        if decision is not None:
            print(json.dumps(decision.hook_output()))
        return 0
    
    
    if __name__ == "__main__":
        sys.exit(main())
    
  • hooks/run-hook.shRunsGitHub
    Read the script
    #!/bin/sh
    # Run a nutmeg research hook only when a research project is active.
    #
    #   sh run-hook.sh <hook script in this folder>   (hook JSON on stdin)
    #
    # With no active project this exits at once, before Python starts, so
    # ordinary nutmeg use and Claude Code's normal permission flow are unchanged.
    # Without python3 >= 3.10 it warns once per repository and exits 0.
    # It uses only shell built-ins until it knows a project is active.
    
    script="$1"
    here="${0%/*}"
    
    # Find research/.active from the project folder up to the repository root.
    # Stop at the filesystem root, a drive root (C:) or after 64 steps.
    dir="${CLAUDE_PROJECT_DIR:-$PWD}"
    steps=0
    while :; do
      if [ -f "$dir/research/.active" ]; then
        break
      fi
      if [ -e "$dir/.git" ]; then
        exit 0
      fi
      parent="${dir%/*}"
      if [ "$parent" = "$dir" ]; then
        parent="${dir%\\*}"
      fi
      steps=$((steps + 1))
      if [ -z "$parent" ] || [ "$parent" = "$dir" ] || [ "$steps" -gt 64 ]; then
        exit 0
      fi
      dir="$parent"
    done
    
    if ! command -v python3 >/dev/null 2>&1 || ! python3 -c 'import sys; sys.exit(sys.version_info < (3, 10))' 2>/dev/null; then
      marker="$dir/research/.python-warned"
      if [ ! -f "$marker" ]; then
        : > "$marker" 2>/dev/null
        printf '%s\n' '{"systemMessage": "nutmeg: a research project is active, but python3 3.10 or newer was not found, so the run gate and checks are off. Install Python 3.10+ to turn them on."}'
      fi
      exit 0
    fi
    
    NUTMEG_REPO_ROOT="$dir" exec python3 "$here/$script"
    
  • hooks/session-start.shRunsGitHub
    Read the script
    #!/bin/sh
    # Print the SessionStart context with the plugin path filled in.
    #
    # The research-project rules (session-start-research.txt, stored JSON-escaped) are added only when the project has a
    # research/ folder, so sessions without research projects do not pay for them.
    #
    # Pure shell, no sed: the path is JSON-escaped (backslashes and quotes) and
    # inserted literally, so characters such as & or | in it cannot corrupt the JSON.
    
    root="${CLAUDE_PLUGIN_ROOT:-${0%/hooks/*}}"
    file="$root/hooks/session-start.json"
    fragment="$root/hooks/session-start-research.txt"
    project="${CLAUDE_PROJECT_DIR:-$PWD}"
    
    # JSON-escape the path, one character at a time.
    newline='
    '
    tab=$(printf '\t')
    cr=$(printf '\r')
    escaped=""
    rest="$root"
    while [ -n "$rest" ]; do
      char="${rest%"${rest#?}"}"
      rest="${rest#?}"
      case "$char" in
        \\) escaped="$escaped\\\\" ;;
        \") escaped="$escaped\\\"" ;;
        "$newline") escaped="$escaped\\n" ;;
        "$tab") escaped="$escaped\\t" ;;
        "$cr") escaped="$escaped\\r" ;;
        *) escaped="$escaped$char" ;;
      esac
    done
    # Any other control character: leave the placeholder for a variable name rather than emit invalid JSON.
    case "$escaped" in
      *[[:cntrl:]]*) escaped='$CLAUDE_PLUGIN_ROOT' ;;
    esac
    
    # replace MARKER VALUE: every MARKER in $text becomes VALUE (no pattern characters are interpreted).
    replace() {
      out=""
      while :; do
        case "$text" in
          *"$1"*)
            out="$out${text%%"$1"*}$2"
            text="${text#*"$1"}"
            ;;
          *)
            break
            ;;
        esac
      done
      text="$out$text"
    }
    
    research=""
    if [ -d "$project/research" ] && [ -f "$fragment" ]; then
      research=$(cat "$fragment") || research=""
    fi
    text=$(cat "$file") || exit 0
    replace "@RESEARCH@" "$research"
    replace "@PLUGIN_ROOT@" "$escaped"
    printf '%s\n' "$text"
    
  • hooks/stop_check.pyGitHub
    Read the script
    """Stop hook: check the active research project's outputs before a step ends.
    
    Called through run-hook.sh, which has already found an active project. Blocks
    the stop once for each new open problem, with the list; a second stop
    (stop_hook_active) is always allowed. Open problems stay in checks.json, and
    publishing refuses until each is fixed or accepted with a reason.
    """
    import json
    import os
    import sys
    from pathlib import Path
    
    sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "core"))
    
    from nutmeg_core import check, project  # noqa: E402
    
    
    def main():
        try:
            data = json.load(sys.stdin)
        except (json.JSONDecodeError, ValueError):
            data = {}
        if data.get("stop_hook_active"):
            return 0
        repo = Path(os.environ.get("NUTMEG_REPO_ROOT") or data.get("cwd") or os.getcwd())
        active = project.active_project(repo)
        if active is None or not check.output_files(active):
            return 0
        try:
            state = check.check(active)
        except Exception as exc:  # never trap the user in a loop because the check broke
            print(json.dumps({"systemMessage": f"nutmeg check could not run: {exc}"}))
            return 0
        new = [f for f in state["open"] if f["id"] not in state["last_blocked"]]
        if not new:
            return 0
        state["last_blocked"] = sorted(set(state["last_blocked"]) | {f["id"] for f in state["open"]})
        check.save_state(active, state)
        name = active.relative_to(repo) if active.is_relative_to(repo) else active
        lines = [f"nutmeg check: {len(state['open'])} open problem(s) in the outputs of {name}:"]
        lines += [f"- {check.describe(f)}" for f in state["open"][:20]]
        if len(state["open"]) > 20:
            lines.append(f"- … {len(state['open']) - 20} more (run `nutmeg check`)")
        lines.append(
            "Fix each one: add the missing claim with its evidence, or correct the output, then run `nutmeg check`. "
            "If the user says a problem is not one, record their reason with "
            "`nutmeg check --accept <id> --reason \"...\"`. Publishing refuses while problems are open."
        )
        print(json.dumps({"decision": "block", "reason": "\n".join(lines)}))
        return 0
    
    
    if __name__ == "__main__":
        sys.exit(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.

@withqwerty
Ships withnutmeg

A Claude Code plugin that makes Claude an expert at football data analytics. Who it's for: Anyone who works with football data — analysts, developers, journalists, researchers, hobbyists.

5611 skills3 agents1 MCP
Get the whole plugin

Stars on GitHub

Momentum

stars in total
56
Star history of withqwerty/nutmeg on GitHub, a chart by star-history.com

More plugins that ship hooks.

last30days-cn
Plugin

last30days-cn

A Claude Code skill that researches what Chinese internet users said recently across Weibo, Xiaohongshu, Bilibili, Zhihu, Douyin, WeChat, Baidu and Toutiao. It ranks posts by engagement and recency and writes a sourced research report.

1 skill1 hook2k
@jesseovo@jesseovo
Install Plugin
episodic-memory
Plugin

episodic-memory

Semantic search over past Claude Code conversations, so a session can recall earlier discussions, decisions, and patterns. It adds a remembering-conversations skill that checks prior sessions before the assistant says it doesn't know.

1 skill+3493
@obra-2@obra-2
Install Plugin
memx
Plugin

memx

Gives Claude Code a long-term memory layer with semantic search, recall injection, and graph entities, letting agents recall, save, inspect, and audit memories through bundled MCP tools.

1 skill+2403
@neoli00@neoli00
Install Plugin
databricks-agent-skills
Plugin

databricks-agent-skill…

Skills and agent plugins that let AI coding agents such as Claude Code work with Databricks, covering AI Functions, Agent Bricks, AI/BI dashboards, GPU training workloads and Databricks Apps design.

33 skills+2376
@databricks@databricks
Install Plugin
pinecone
Plugin

pinecone

A lightweight plugin that integrates Pinecone vector database capabilities directly into Claude Code, enabling semantic search, index management, and RAG (Retrieval Augmented Generation) workflows.

9 skills+369
@pinecone-io@pinecone-io
Install Plugin
magi
Plugin

magi

A research workflow plugin for Claude Code that turns folders of papers, notes and drafts into a linked, Obsidian-compatible knowledge base. It adds skills to adopt existing material, compile sources into concept cards, answer questions with citations, and write up unattended runs.

11 skills1 hook7
@misaka16384@misaka16384
Install Plugin
subrosa
Plugin

subrosa

Archives every Claude Code session locally in a searchable SQLite store and saves durable facts into persistent memory. Adds skills to flush and review memory at session start or before clearing a conversation.

2 skills5 hooks5
@ij5a@ij5a
Install Plugin
ainl-cortex
Plugin

ainl-cortex

Adds graph-based memory to Claude Code that records past interactions and learns recurring patterns across sessions. Runs as hooks and an MCP server.

7 hooks1 MCP4
@sbhooley@sbhooley
Install Plugin

Questions about the hooks in nutmeg.

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