Skip to content
Development
Hook

Hooks

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

From plugin
loop-kit
87 skills6 agents2 hooks
Install
> /plugin marketplace add cbdreamer11/CB-loop-kit-claude-plugin
> /plugin install loop-kit@loop-kit

Ships with loop-kit. Installing the plugin gets these hooks.

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/guard-publish.sh"
Read hooks/hooks.json

Where it lives

  • hooks/guard-publish.shRunsGitHub
    Read the script
    #!/bin/sh
    # PreToolUse hook (matcher: Bash) — the only two laws a hook can actually enforce.
    #
    # Blocks: pushing to the protected branch, and the commands this project declared
    # forbidden in .loop/VERIFY.md. Exit 2 blocks the call and shows stderr to the agent.
    #
    # What this canNOT do, and the README says so plainly: it cannot tell whether
    # something was really verified. That is not enforceable by any hook — only
    # deterministic facts are. Do not pretend otherwise.
    #
    # Bypassable by design (--safe-mode, disabled hooks, managed settings). This is a
    # guardrail against an honest mistake, not a security boundary.
    #
    # Known limitation, stated rather than hidden: matching is done on the payload text, so a
    # command that merely CONTAINS a forbidden string is blocked too — a test, a grep, or an
    # echo that quotes it. Distinguishing "runs a push" from "mentions a push" needs real
    # command parsing, which is not worth a fragile POSIX implementation. When it fires on a
    # false positive, rephrase the command or let the owner run it.
    
    ROOT=${CLAUDE_PROJECT_DIR:-.}
    VERIFY="$ROOT/.loop/VERIFY.md"
    PAYLOAD=$(cat)
    
    # Only enforce in a project that actually adopted the loop. Installing this plugin must
    # never change how git behaves in unrelated repositories — that is how a useful guard
    # becomes the reason someone uninstalls it. (Caught in real use: with no VERIFY.md the
    # guard fell back to protecting `main` everywhere and blocked a legitimate push in a
    # project that had never run setup.)
    #
    # Note the scope limit: the hook can only see the session's project directory. If you
    # work on repository A in a session rooted at repository B, A's rules are not the ones
    # being applied. Declared, not hidden.
    [ -f "$VERIFY" ] || exit 0
    
    # Protected branch, as declared in VERIFY.md. If the field is still the template blank,
    # protect the usual suspects — the project adopted the loop, it just has not filled it in.
    BRANCH=$(sed -n 's/^- Protected branch[^`]*`\([^`]*\)`.*/\1/p' "$VERIFY" 2>/dev/null | head -1)
    case "$BRANCH" in ''|_____*) BRANCH="main master" ;; esac
    
    case "$PAYLOAD" in
      *"git push"*)
        for b in $BRANCH; do
          case "$PAYLOAD" in
            *"$b"*)
              echo "Blocked: pushing to the protected branch '$b' is the owner's action, not the agent's." >&2
              echo "Commit on a working branch instead, and report that it is ready to publish." >&2
              exit 2
              ;;
          esac
        done
        # A bare 'git push' on the protected branch is the same thing.
        CUR=$(git -C "$ROOT" branch --show-current 2>/dev/null)
        for b in $BRANCH; do
          if [ "$CUR" = "$b" ]; then
            echo "Blocked: the current branch is the protected branch '$b'. Publishing is the owner's call." >&2
            exit 2
          fi
        done
        ;;
    esac
    
    # Commands this project declared forbidden, comma-separated inside backticks.
    #
    # Note for anyone editing this: do NOT loop with `... | while read`. A pipeline runs
    # in a subshell, so `exit 2` would only leave the subshell and the hook would print
    # "Blocked" while returning 0 — a guard that says it blocks and does not. (That bug
    # was in the first version of this file and was caught by actually running it.)
    if [ -f "$VERIFY" ]; then
      FORBIDDEN=$(sed -n 's/^- Forbidden commands[^`]*`\([^`]*\)`.*/\1/p' "$VERIFY" 2>/dev/null | tr ',' '\n')
      OLDIFS=$IFS
      IFS='
    '
      for cmd in $FORBIDDEN; do
        IFS=$OLDIFS
        cmd=$(printf '%s' "$cmd" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
        case "$cmd" in
          ''|_____*) IFS='
    '; continue ;;
        esac
        case "$PAYLOAD" in
          *"$cmd"*)
            echo "Blocked: '$cmd' is listed as a forbidden command in .loop/VERIFY.md." >&2
            echo "If it genuinely needs to run, the owner runs it." >&2
            exit 2
            ;;
        esac
        IFS='
    '
      done
      IFS=$OLDIFS
    fi
    
    exit 0
    
  • hooks/session-start.shRunsGitHub
    Read the script
    #!/bin/sh
    # SessionStart hook — mechanizes step 0 ("locate yourself").
    #
    # Whatever this prints on stdout is added to the session's context before the first
    # token, so a session lands already oriented: what is next, what is parked, what
    # landed recently, and which files another session may be holding.
    #
    # Never fails the session: exits 0 no matter what. If it cannot find the loop, it
    # stays quiet rather than shouting at a project that does not use it.
    
    ROOT=${CLAUDE_PROJECT_DIR:-.}
    STATE="$ROOT/.loop/STATE.md"
    
    [ -f "$STATE" ] || exit 0
    
    # Placeholder lines from the template (they contain <angle brackets>) are filtered
    # out everywhere: an unfilled template line read as a real gap sends a session
    # chasing work that does not exist.
    NOISE='<[^>]*>'
    
    echo "## Where this project stands (from .loop/STATE.md)"
    echo
    sed -n '/^## Epic:/,/^### Slices/p' "$STATE" | grep -vE "$NOISE" | sed -n '1,15p'
    echo
    echo "### Open items, gaps and blocks"
    OPEN=$(grep -E '^(\[ \]|GAP|PARKED|BLOCKED)' "$STATE" | grep -vE "$NOISE" | sed -n '1,20p')
    [ -n "$OPEN" ] && echo "$OPEN" || echo "(none recorded)"
    echo
    CLOSED=$(grep -E '^\[x\]' "$STATE" | grep -vE "$NOISE" | sed -n '1,10p')
    if [ -n "$CLOSED" ]; then
      echo "### Already done — do not rebuild these"
      echo "$CLOSED"
      echo
    fi
    
    if [ -f "$ROOT/.loop/GOTCHAS.md" ]; then
      echo "### Traps this project has already sprung (see .loop/GOTCHAS.md)"
      grep -E '^## ' "$ROOT/.loop/GOTCHAS.md" | sed -n '1,10p'
      echo
    fi
    
    echo "### Recent commits"
    git -C "$ROOT" log --oneline -15 2>/dev/null
    echo
    
    HOT=$(git -C "$ROOT" status --porcelain 2>/dev/null | sed -n '1,15p')
    if [ -n "$HOT" ]; then
      echo "### Uncommitted files — may belong to a parallel session, do not touch what is not yours"
      echo "$HOT"
      echo
    fi
    
    echo "Before building anything: read .loop/VERIFY.md and follow the loop skill."
    echo "Anything already committed is not rebuilt. Confirm against the code, not against a document."
    
    exit 0
    

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 withloop-kit

A working method for building real software with coding agents, across many sessions.

Get the whole plugin
Stats
8
Stars
1
Forks
Maintained
Maintenance
Shell
Language
MIT
License
1mo ago
Last commit
1mo ago
Created

Repo: cbdreamer11/CB-loop-kit-claude-plugin