Skip to content
Agent Orchestration
Hook

Hooks

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

From plugin
super-board
67010 skills7 hooks

Claude Code

> /plugin marketplace add EricTechPro/super-board
> /plugin install super-board@super-board

Where it lives

  • hooks/cleanup-wt.pyGitHub
    Read the script
    #!/usr/bin/env python3
    """cleanup-wt -- remove worktrees and branches whose work is already on a base branch.
    
    A hook, not a user skill: installed at .claude/hooks/cleanup-wt.py, run by
    super-board-merge-gate.sh after each merge (--post-merge) and by the default SessionStart
    hook (--auto). A manual dry run (no flags) still works for a one-off look.
    
    Usage (from the repo root or any of its worktrees):
      cleanup-wt.py                 dry run: print the plan, write the recovery TSV
      cleanup-wt.py --apply         act on the plan
        --base <name>     a base branch; repeatable. Default: detected (see below)
        --force           also remove locked, fresh, open-PR and unmerged *worktrees*.
                          Their branches are always kept: unmerged work is never deleted.
        --local-only      skip remote branches       --remote-only   skip worktrees and local branches
        --no-fetch        use refs as they are       --tsv <file>    where the recovery TSV goes
        --post-merge      merge-gate mode: --apply --local-only, never wip-commit, never force,
                          silent unless it acted
        --auto            SessionStart mode: --post-merge, but only when a base tip moved
                          since the last --auto run (the first run only records the tips)
    
    Bases, when no --base is given: the remote default branch (origin/HEAD), else
    init.defaultBranch, else main/master -- plus staging, develop and dev when they exist.
    CLEANUP_WT_BASES="a,b" overrides detection like repeated --base.
    
    A branch is merged when its tip is an ancestor of a base, every patch is already in a
    base (git cherry), one commit equal to the whole branch is in a base (squash probe), or
    gh reports a merged PR at that tip. Bases, master/main and the branch checked out in the
    main checkout are never touched, nor is the worktree this runs in.
    
    Every run writes <git-common-dir>/cleanup-wt/<timestamp>.tsv before acting:
    when, kind, name, sha, path, action, reason. Restore with
      git branch <name> <sha>   |   git push origin <sha>:refs/heads/<name>
    
    Adapted from BookKeepingApp's cleanup-wt (Eric Tech), generalised for any repo.
    """
    import datetime
    import json
    import os
    import subprocess
    import sys
    import time
    
    KEEP, REMOVE, REVIEW = "KEEP", "REMOVE", "REVIEW"
    ALLOWED_DIR = os.path.join(".claude", "worktrees")
    FRESH_SECS = 24 * 3600
    ALWAYS_PROTECTED = {"main", "master", "HEAD"}
    EXTRA_BASES = ["staging", "develop", "dev"]
    
    # Drop GIT_DIR and friends (inherited from a git hook they would redirect every
    # `git -C`), never prompt, and give the squash probe's commit-tree an identity.
    GIT_ENV = {k: v for k, v in os.environ.items() if not k.startswith("GIT_")}
    GIT_ENV.update(
        GIT_TERMINAL_PROMPT="0",
        GIT_OPTIONAL_LOCKS="0",
        GIT_AUTHOR_NAME="cleanup-wt",
        GIT_AUTHOR_EMAIL="cleanup-wt@localhost",
        GIT_COMMITTER_NAME="cleanup-wt",
        GIT_COMMITTER_EMAIL="cleanup-wt@localhost",
    )
    
    
    def git(cwd, *args, timeout=60):
        try:
            r = subprocess.run(["git", "-C", cwd] + list(args), capture_output=True, text=True,
                               env=GIT_ENV, timeout=timeout)
            return r.returncode, r.stdout.strip(), r.stderr.strip()
        except (subprocess.TimeoutExpired, OSError) as e:
            return 1, "", str(e)
    
    
    def git_out(cwd, *args):
        code, out, _ = git(cwd, *args)
        return out if code == 0 else None
    
    
    def real(p):
        return os.path.realpath(p) if os.path.exists(p) else p
    
    
    # ---- facts ------------------------------------------------------------------
    
    def context(cwd):
        top = git_out(cwd, "rev-parse", "--show-toplevel")
        if not top:
            return None
        common = git_out(cwd, "rev-parse", "--path-format=absolute", "--git-common-dir")
        wts = list_worktrees(top)
        root = real(wts[0]["path"]) if wts else real(top)
        return {"cwd": top, "top": real(top), "root": root, "common": common}
    
    
    def list_worktrees(cwd):
        out = git_out(cwd, "worktree", "list", "--porcelain") or ""
        wts = []
        for block in out.split("\n\n"):
            wt = {"path": "", "head": "", "branch": None, "locked": False, "prunable": False, "bare": False}
            for line in block.splitlines():
                key, _, val = line.partition(" ")
                if key == "worktree":
                    wt["path"] = val
                elif key == "HEAD":
                    wt["head"] = val
                elif key == "branch":
                    wt["branch"] = val[len("refs/heads/"):] if val.startswith("refs/heads/") else val
                elif key in ("locked", "prunable", "bare"):
                    wt[key] = True
            if wt["path"]:
                wts.append(wt)
        return wts
    
    
    def ref_exists(cwd, ref):
        return git_out(cwd, "rev-parse", "-q", "--verify", ref) is not None
    
    
    def base_names(cwd, given):
        """The base branch names: given ones, else the detected default plus common integration branches."""
        if not given and os.environ.get("CLEANUP_WT_BASES"):
            given = [b.strip() for b in os.environ["CLEANUP_WT_BASES"].split(",") if b.strip()]
        if given:
            return list(dict.fromkeys(given))
        names = []
        head = git_out(cwd, "symbolic-ref", "-q", "--short", "refs/remotes/origin/HEAD")
        if head and head.startswith("origin/"):
            names.append(head[len("origin/"):])
        else:
            init = git_out(cwd, "config", "--get", "init.defaultBranch")
            for n in ([init] if init else []) + ["main", "master"]:
                if ref_exists(cwd, "refs/remotes/origin/" + n) or ref_exists(cwd, "refs/heads/" + n):
                    names.append(n)
                    break
        for n in EXTRA_BASES:
            if ref_exists(cwd, "refs/remotes/origin/" + n) or ref_exists(cwd, "refs/heads/" + n):
                names.append(n)
        return list(dict.fromkeys(names))
    
    
    def base_refs(cwd, names):
        """Existing base refs, origin first, deduplicated by SHA."""
        seen, found = set(), []
        for n in names:
            for ref in ("refs/remotes/origin/" + n, "refs/heads/" + n):
                sha = git_out(cwd, "rev-parse", "-q", "--verify", ref)
                i
  • hooks/guard-delete-outside.pyGitHub
    Read the script
    #!/usr/bin/env python3
    """PreToolUse(Bash) guard: never delete files outside the project.
    
    Denies `rm`, `rmdir`, `unlink`, `find ... -delete` / `find ... -exec rm`, and `git clean`
    when a target resolves outside $CLAUDE_PROJECT_DIR, and always when it is `/` or the home
    directory itself. Paths are resolved against the hook's `cwd`, honouring `cd X &&`; `~`,
    `$HOME` and other set variables are expanded the way the shell would. A word whose value
    cannot be known before it runs (a `$(...)`, or a variable assigned earlier in the same
    command) is allowed: the guard catches the literal mistakes, not every possible one.
    
    Temp roots stay deletable inside (not the roots themselves): /tmp, /private/tmp,
    /var/folders and $TMPDIR. `git clean` is denied only when it would run in a directory
    outside the project.
    
    Its own errors never block a tool call: anything unexpected exits 0 with no output.
    """
    import json
    import os
    import re
    import sys
    
    RM_CMDS = {"rm", "rmdir", "unlink", "trash"}
    TEMP_ROOTS = ["/tmp", "/private/tmp", "/var/folders", "/private/var/folders"]
    
    
    def segments(command):
        """Split a shell command into simple-command token lists. Quotes are honoured; nothing runs."""
        out, tokens, cur, has, quote = [], [], "", False, None
        i = 0
    
        def push_token():
            nonlocal cur, has
            if has:
                tokens.append(cur)
            cur, has = "", False
    
        while i < len(command):
            c = command[i]
            if quote:
                if c == quote:
                    quote = None
                elif c == "\\" and quote == '"' and i + 1 < len(command):
                    i += 1
                    cur += command[i]
                else:
                    if c == "$" and quote == "'":
                        cur += "\0"  # single-quoted $ is literal; mark it so expand() leaves it
                    else:
                        cur += c
            elif c in "'\"":
                quote, has = c, True
            elif c == "\\" and i + 1 < len(command):
                i += 1
                cur += command[i]
                has = True
            elif c in " \t":
                push_token()
            elif c in ";\n|&()":
                push_token()
                if tokens:
                    out.append(tokens)
                tokens = []
            else:
                cur += c
                has = True
            i += 1
        push_token()
        if tokens:
            out.append(tokens)
        return out
    
    
    def expand(word, env, local):
        """Expand ~ and $VAR like the shell. None when the value cannot be known before it runs."""
        if "$(" in word or "`" in word:
            return None
        s = re.sub(r"^~(?=/|$)", lambda m: env.get("HOME", "~"), word)
        unknowable = []
    
        def var(m):
            name = m.group(1) or m.group(2)
            if name in local:
                unknowable.append(name)
            return env.get(name, "")
    
        s = re.sub(r"\$\{([A-Za-z_][A-Za-z0-9_]*)\}|\$([A-Za-z_][A-Za-z0-9_]*)", var, s)
        if unknowable:
            return None
        return s.replace("\0", "$")
    
    
    def under(path, root):
        return path == root or path.startswith(root.rstrip(os.sep) + os.sep)
    
    
    def verdict(path, project, env):
        """None when the deletion target is fine, else why it is not."""
        home = os.path.normpath(env.get("HOME", "")) if env.get("HOME") else None
        forms = {path, os.path.join(os.path.realpath(os.path.dirname(path)), os.path.basename(path))}
        if any(p == os.sep for p in forms) or (home and any(p == home for p in forms)):
            return "is the filesystem root or your home directory"
        roots = {project, os.path.realpath(project)}
        if any(under(p, r) for p in forms for r in roots):
            return None
        temps = TEMP_ROOTS + ([os.path.normpath(env["TMPDIR"])] if env.get("TMPDIR") else [])
        if any(under(p, t) and p.rstrip(os.sep) != t.rstrip(os.sep) for p in forms for t in temps):
            return None
        return "is outside the project (%s)" % project
    
    
    def targets(command, cwd, env):
        """[(command_name, raw_word, resolved_or_None)] for every deletion target."""
        found, d, local = [], cwd, set()
        for tokens in segments(command):
            while tokens and re.match(r"^[A-Za-z_][A-Za-z0-9_]*=", tokens[0]):
                local.add(tokens[0].split("=", 1)[0])
                tokens = tokens[1:]
            while tokens and tokens[0] in ("sudo", "command", "nice", "time", "xargs"):
                tokens = tokens[1:]
            if tokens and tokens[0] in ("export", "local", "read", "for"):
                local.update(re.sub(r"=.*", "", t) for t in tokens[1:] if re.match(r"^[A-Za-z_]", t))
                continue
            if not tokens:
                continue
            cmd, args = os.path.basename(tokens[0]), tokens[1:]
            if cmd == "cd":
                target = expand(args[0], env, local) if args else env.get("HOME")
                d = os.path.normpath(os.path.join(d, target)) if target else d
                continue
    
            def resolve(word, base=None):
                ex = expand(word, env, local)
                if ex is None or ex == "":
                    return None
                return os.path.normpath(os.path.join(base or d, ex))
    
            if cmd in RM_CMDS:
                words, end = [], False
                for a in args:
                    if not end and a == "--":
                        end = True
                    elif end or not a.startswith("-") or a == "-":
                        words.append(a)
                for w in words:
                    found.append((cmd, w, resolve(w)))
            elif cmd == "find":
                if "-delete" in args or any(a == "-exec" and i + 1 < len(args) and os.path.basename(args[i + 1]) in RM_CMDS
                                            for i, a in enumerate(args)):
                    for w in args:
                        if w.startswith("-") or w in ("!", "(", ")"):
                            break
                        found.append(("find", w, resolve(w)))
            elif cmd == "git":
                git_dir, i = d, 0
                while i < len(args) and args[i].startswith("-"):
                    if args[i] == "-C" and i + 1 < len(args):
                        git_dir = resolve(args[i + 1]) or git_dir
          
  • hooks/guard-key-literals.pyGitHub
    Read the script
    #!/usr/bin/env python3
    """Guard against live credentials entering the codebase.
    
    guard-secrets stops secrets getting OUT (reading dotenv files). This stops them
    getting IN: a key pasted into source, a config file, or a script. Wired on two events:
      PreToolUse  (Edit|MultiEdit|Write|NotebookEdit|Bash) -> deny before the key hits disk
      PostToolUse (Edit|MultiEdit|Write|NotebookEdit)      -> catch what got through another
                                                              path and tell the agent to fix it
    
    It never prints the matched value -- that would put the key in the transcript, the
    thing both guards exist to prevent. Line numbers only. Its own errors never block.
    
    Adapted from ai-builder-starter-kit's .agents/hooks/guard-key-literals.sh (Eric Tech).
    """
    import json
    import re
    import sys
    
    Q = "[\"']"
    KEY_RE = re.compile("|".join([
        r"sk-ant-[A-Za-z0-9_-]{20,}",
        r"sk-proj-[A-Za-z0-9_-]{20,}",
        r"sk-[A-Za-z0-9]{24,}",
        r"AKIA[0-9A-Z]{16}",
        r"ASIA[0-9A-Z]{16}",
        r"gh[pousr]_[A-Za-z0-9]{30,}",
        r"github_pat_[A-Za-z0-9_]{30,}",
        r"xox[baprs]-[A-Za-z0-9-]{12,}",
        r"AIza[A-Za-z0-9_-]{35}",
        r"sbp_[a-f0-9]{40}",
        r"r8_[A-Za-z0-9]{35,}",
        r"hf_[A-Za-z0-9]{30,}",
        r"-----BEGIN [A-Z ]*PRIVATE KEY-----",
        r"(?:api[_-]?key|secret|token|password|passwd|bearer)[A-Za-z_]*" + Q + r"?\s*[:=]\s*" + Q
        + r"[A-Za-z0-9_/+=.-]{24,}" + Q,
    ]), re.IGNORECASE)
    
    # A line that reads from the environment is the correct pattern, not a finding.
    SAFE_RE = re.compile(r"process\.env|import\.meta\.env|Deno\.env|os\.environ|getenv|ENV\[|System\.getenv"
                         r"|secrets\.|vars\.|\$\{?[A-Z][A-Z0-9_]*\}?|%[A-Z][A-Z0-9_]*%")
    # Obvious stand-ins: templates and docs are full of these on purpose.
    FAKE_RE = re.compile(r"your[-_]|my[-_]key|xxxx|<[A-Za-z_]|\.\.\.|placeholder|example|sample|template"
                         r"|changeme|change[-_]me|dummy|fake|redacted|REPLACE|TODO|\*\*\*\*", re.IGNORECASE)
    TEMPLATE_FILES = (".env.example", ".env.sample", ".env.template")
    
    
    def visible_text(payload):
        event = payload.get("hook_event_name") or ""
        tool = payload.get("tool_name") or ""
        ti = payload.get("tool_input") or {}
        if event == "PreToolUse":
            if tool == "Write":
                return ti.get("content") or ""
            if tool in ("Edit", "MultiEdit"):
                parts = [ti.get("new_string") or ""] + [e.get("new_string") or "" for e in ti.get("edits") or []]
                return "\n".join(parts)
            if tool == "NotebookEdit":
                return ti.get("new_source") or ""
            if tool == "Bash":  # heredocs, echo >> file, sed -i: the other tools never see these
                return ti.get("command") or ""
            return ""
        if event == "PostToolUse":
            path = ti.get("file_path") or ""
            if not path or path.endswith(TEMPLATE_FILES):
                return ""
            try:
                with open(path, errors="replace") as fh:
                    return fh.read()
            except OSError:
                return ""
        return ""
    
    
    def hit_lines(text):
        hits = []
        for n, line in enumerate(text.splitlines(), 1):
            if KEY_RE.search(line) and not SAFE_RE.search(line) and not FAKE_RE.search(line):
                hits.append(n)
                if len(hits) == 5:
                    break
        return hits
    
    
    def main():
        try:
            payload = json.loads(sys.stdin.read() or "{}")
            hits = hit_lines(visible_text(payload))
            if not hits:
                return
            reason = ("Possible live credential in this change, at line(s) %s. Do not print or repeat the value. "
                      "Move it to the local dotenv file, read it from the environment at the call site, and add only "
                      "the key NAME to the checked-in example env file. If this is a placeholder or test fixture, make "
                      "it obviously fake (prefix it with YOUR_ or example_) and retry. Checked by guard-key-literals."
                      % ",".join(map(str, hits)))
            if payload.get("hook_event_name") == "PreToolUse":
                out = {"hookEventName": "PreToolUse", "permissionDecision": "deny", "permissionDecisionReason": reason}
            else:
                out = {"hookEventName": "PostToolUse", "additionalContext": reason}
            print(json.dumps({"hookSpecificOutput": out}))
        except Exception:
            pass
    
    
    if __name__ == "__main__":
        main()
    
  • hooks/guard-protected-push.pyGitHub
    Read the script
    #!/usr/bin/env python3
    """PreToolUse(Bash) guard: no direct or force pushes to a protected base branch.
    
    Opt-in. `super-board onboard` asks once ("Block direct/force pushes to main?") and
    `install.sh --protect-main` wires it. Lanes push feature branches and merge through
    `super-board-merge-gate.sh` (`gh pr merge`), so nothing in the loop needs to push a base.
    
    Protected: main, master, every `base_branch` in .claude/super-board/configs/*.json, and
    any name in SB_PROTECTED_BRANCHES (comma-separated). A `git push` is denied when it would
    update or delete one of them: an explicit refspec (`feat:main`, `+main`, `HEAD:refs/heads/main`,
    `--delete main`), `--all` / `--mirror`, or a bare `git push` / `git push origin HEAD` while the
    protected branch is checked out. Force flags (`-f`, `--force`, `--force-with-lease`) are named in
    the reason; force pushes to feature branches stay allowed (the rebase pass needs them).
    
    SB_ALLOW_PROTECTED_PUSH=1 in Claude Code's environment turns it off for a session.
    Its own errors never block a tool call: anything unexpected exits 0 with no output.
    """
    import glob
    import json
    import os
    import re
    import subprocess
    import sys
    
    GIT_VALUE_OPTS = {"-c", "--git-dir", "--work-tree", "--namespace", "--exec-path", "--config-env"}
    PUSH_VALUE_OPTS = {"--repo", "--receive-pack", "--exec", "-o", "--push-option", "--signed"}
    FORCE = re.compile(r"^(-f|--force|--force-with-lease(=.*)?|--force-if-includes)$")
    
    
    def segments(command):
        """Split a shell command into simple-command token lists. Quotes are honoured; nothing runs."""
        out, tokens, cur, has, quote = [], [], "", False, None
        i = 0
    
        def push_token():
            nonlocal cur, has
            if has:
                tokens.append(cur)
            cur, has = "", False
    
        while i < len(command):
            c = command[i]
            if quote:
                if c == quote:
                    quote = None
                elif c == "\\" and quote == '"' and i + 1 < len(command):
                    i += 1
                    cur += command[i]
                else:
                    cur += c
            elif c in "'\"":
                quote, has = c, True
            elif c == "\\" and i + 1 < len(command):
                i += 1
                cur += command[i]
                has = True
            elif c in " \t":
                push_token()
            elif c in ";\n|&()":
                push_token()
                if tokens:
                    out.append(tokens)
                tokens = []
            else:
                cur += c
                has = True
            i += 1
        push_token()
        if tokens:
            out.append(tokens)
        return out
    
    
    def branch_name(ref):
        ref = ref.lstrip("+")
        return ref[len("refs/heads/"):] if ref.startswith("refs/heads/") else ref
    
    
    def pushes(command, cwd):
        """[(git_dir, flags, positional)] for every `git push` in the command."""
        found, d = [], cwd
        for tokens in segments(command):
            while tokens and re.match(r"^[A-Za-z_][A-Za-z0-9_]*=", tokens[0]):
                tokens = tokens[1:]
            if tokens and tokens[0] in ("sudo", "command", "env"):
                tokens = tokens[1:]
            if not tokens:
                continue
            cmd, args = tokens[0], tokens[1:]
            if cmd == "cd" and args and "$" not in args[0]:
                d = os.path.normpath(os.path.join(d, os.path.expanduser(args[0])))
                continue
            if os.path.basename(cmd) != "git":
                continue
            git_dir, i = d, 0
            while i < len(args) and args[i].startswith("-"):
                if args[i] == "-C" and i + 1 < len(args):
                    git_dir = os.path.normpath(os.path.join(git_dir, os.path.expanduser(args[i + 1])))
                    i += 2
                elif args[i] in GIT_VALUE_OPTS:
                    i += 2
                else:
                    i += 1
            if i >= len(args) or args[i] != "push":
                continue
            flags, positional, j = [], [], i + 1
            while j < len(args):
                a = args[j]
                if a == "--":
                    positional += args[j + 1:]
                    break
                if a in PUSH_VALUE_OPTS:
                    j += 1
                elif a.startswith("-"):
                    flags.append(a)
                elif not re.match(r"^\d*[<>]", a):
                    positional.append(a)
                j += 1
            found.append((git_dir, flags, positional))
        return found
    
    
    def current_branch(d):
        env = {k: v for k, v in os.environ.items() if not k.startswith("GIT_")}
        try:
            r = subprocess.run(["git", "-C", d, "symbolic-ref", "--short", "-q", "HEAD"],
                               capture_output=True, text=True, env=env, timeout=10)
            return r.stdout.strip() or None
        except (OSError, subprocess.TimeoutExpired):
            return None
    
    
    def protected_branches(project_dir):
        names = {"main", "master"}
        for p in glob.glob(os.path.join(project_dir, ".claude", "super-board", "configs", "*.json")):
            try:
                base = json.load(open(p)).get("base_branch")
                if isinstance(base, str) and base:
                    names.add(base)
            except (OSError, ValueError, AttributeError):
                pass
        names.update(n.strip() for n in os.environ.get("SB_PROTECTED_BRANCHES", "").split(",") if n.strip())
        return names
    
    
    def check(command, cwd, protected, branch_of):
        """None when allowed, else the deny reason. branch_of(dir) -> checked-out branch or None."""
        if not re.search(r"\bpush\b", command):
            return None
        for git_dir, flags, positional in pushes(command, cwd):
            force = any(FORCE.match(f) for f in flags) or any(p.startswith("+") for p in positional[1:])
            deleting = any(f in ("-d", "--delete") for f in flags)
            hit = None
            if "--all" in flags or "--mirror" in flags or "--branches" in flags:
                hit = "every branch (" + next(f for f in flags if f in ("--all", "--mirror", "--branches")) + ")"
            elif len(positional) <= 1:
                if not deleting and "--tags" not in flags:
                    cur = branch_of(git_dir)
                    if cur in protected:
         
  • hooks/guard-secrets.pyGitHub
    Read the script
    #!/usr/bin/env python3
    """PreToolUse(Bash|Read|Grep) guard: refuse to read or pipe secret files.
    
    Stops secrets getting OUT -- `cat .env`, `source .env.local`, `grep KEY .env | curl`,
    or the Read tool opening a dotenv file -- so a key never lands in the transcript.
    `.env.example` and friends are deliberately allowed: that file is how an agent
    learns which keys exist. Its own errors never block a tool call.
    
    Adapted from ai-builder-starter-kit's .agents/hooks/guard-secrets.sh (Eric Tech).
    """
    import json
    import re
    import sys
    
    B = r"(?:^|[^A-Za-z0-9._-])"   # boundary before a name
    E = r"(?:[^A-Za-z0-9._-]|$)"   # boundary after a name
    SECRET_RE = re.compile("|".join([
        B + r"\.env" + E,
        r"\.env\.(?:local|production|prod|staging|development|dev|test)" + E,
        B + r"id_(?:rsa|ed25519|ecdsa)" + E,
        B + r"\.npmrc" + E,
        B + r"credentials(?:\.json)?" + E,
        r"service-account[A-Za-z0-9._-]*\.json",
        r"\.pem" + E,
    ]))
    
    # The one sanctioned way to ask "is KEY set?": super-board-env-check.sh prints
    # present / empty / missing per key and never a value. Allowed only as a single
    # plain call -- no pipe, redirect, chain or substitution can ride along.
    ENV_CHECK = re.compile(
        r"^[ \t]*(?:(?:bash|sh)[ \t]+)?(?:[A-Za-z0-9_./-]*/)?super-board-env-check\.sh"
        r"(?:[ \t]+[A-Za-z0-9_./=-]+)*[ \t]*\Z")
    
    REASON = ("Blocked by guard-secrets: this touches a secret file. Read the checked-in example env "
              "file (.env.example) for the key names, or ask the user to run the command themselves "
              "with the ! prefix.")
    
    
    def subject(payload):
        """The text this tool call can expose: a shell command or a file path."""
        tool = payload.get("tool_name") or ""
        ti = payload.get("tool_input") or {}
        if tool == "Bash":
            return ti.get("command") or ""
        if tool == "Read":
            return ti.get("file_path") or ""
        if tool == "Grep":
            return " ".join(str(ti.get(k) or "") for k in ("path", "glob"))
        return ""
    
    
    def main():
        try:
            payload = json.loads(sys.stdin.read() or "{}")
            text = subject(payload)
            if payload.get("tool_name") == "Bash" and ENV_CHECK.match(text):
                return
            if text and SECRET_RE.search(text):
                print(json.dumps({"hookSpecificOutput": {"hookEventName": "PreToolUse",
                                                         "permissionDecision": "deny",
                                                         "permissionDecisionReason": REASON}}))
        except Exception:
            pass  # fail open rather than block every call in the project
    
    
    if __name__ == "__main__":
        main()
    
  • hooks/guard-worktree-path.pyGitHub
    Read the script
    #!/usr/bin/env python3
    """PreToolUse(Bash) guard: a git worktree may only live under <repo>/.claude/worktrees/.
    
    A worktree created next to the repo (`git worktree add ../proj-feature`) sits outside
    every sweep -- cleanup-wt, Claude Code's own -- long after its PR merges. Claude
    Code's docs show that very pattern, so it keeps coming back.
    
    Reads the Bash command, finds every `git worktree add` / `git worktree move`,
    resolves its target (honouring `cd X &&` and `git -C X`) and denies the call when the
    target is outside `.claude/worktrees/` of the repo git will run in. Its own errors
    never block a tool call: anything unexpected exits 0 with no output.
    
    Adapted from BookKeepingApp's .claude/hooks/guard-worktree-path.js (Eric Tech).
    """
    import json
    import os
    import re
    import subprocess
    import sys
    
    ALLOWED_DIR = os.path.join(".claude", "worktrees")
    ADD_VALUE_OPTS = {"-b", "-B", "--reason"}
    GIT_VALUE_OPTS = {"-c", "--git-dir", "--work-tree", "--namespace", "--exec-path", "--config-env"}
    
    
    def segments(command):
        """Split a shell command into simple-command token lists. Quotes are honoured; nothing runs."""
        out, tokens, cur, has, quote = [], [], "", False, None
        i = 0
    
        def push_token():
            nonlocal cur, has
            if has:
                tokens.append(cur)
            cur, has = "", False
    
        while i < len(command):
            c = command[i]
            if quote:
                if c == quote:
                    quote = None
                elif c == "\\" and quote == '"' and i + 1 < len(command):
                    i += 1
                    cur += command[i]
                else:
                    cur += c
            elif c in "'\"":
                quote, has = c, True
            elif c == "\\" and i + 1 < len(command):
                i += 1
                cur += command[i]
                has = True
            elif c in " \t":
                push_token()
            elif c in ";\n|&()":
                push_token()
                if tokens:
                    out.append(tokens)
                tokens = []
            else:
                cur += c
                has = True
            i += 1
        push_token()
        if tokens:
            out.append(tokens)
        return out
    
    
    def expand(word, env):
        """Expand ~, $VAR, ${VAR}. None when a variable is unknown or a substitution is present."""
        s = re.sub(r"^~(?=/|$)", lambda m: env.get("HOME", "~"), word)
        unknown = []
    
        def var(m):
            name = m.group(1) or m.group(2)
            if name not in env:
                unknown.append(name)
            return env.get(name, "")
    
        s = re.sub(r"\$\{([A-Za-z_][A-Za-z0-9_]*)\}|\$([A-Za-z_][A-Za-z0-9_]*)", var, s)
        if unknown or "$(" in s or "`" in s:
            return None
        return s
    
    
    def worktree_targets(command, cwd, env):
        """[(raw, git_dir, resolved_or_None)] for every worktree add/move target."""
        found, d = [], cwd
        for tokens in segments(command):
            while tokens and re.match(r"^[A-Za-z_][A-Za-z0-9_]*=", tokens[0]):
                tokens = tokens[1:]
            if not tokens:
                continue
            cmd, args = tokens[0], tokens[1:]
            if cmd == "cd":
                target = expand(args[0], env) if args else env.get("HOME")
                if target:
                    d = os.path.normpath(os.path.join(d, target))
                continue
            if os.path.basename(cmd) != "git":
                continue
            git_dir, i = d, 0
            while i < len(args) and args[i].startswith("-"):
                if args[i] == "-C":
                    target = expand(args[i + 1] if i + 1 < len(args) else "", env)
                    if target:
                        git_dir = os.path.normpath(os.path.join(git_dir, target))
                    i += 2
                elif args[i] in GIT_VALUE_OPTS:
                    i += 2
                else:
                    i += 1
            if i >= len(args) or args[i] != "worktree" or i + 1 >= len(args):
                continue
            sub = args[i + 1]
            if sub not in ("add", "move"):
                continue
            positional, j = [], i + 2
            while j < len(args):
                a = args[j]
                if a == "--":
                    positional += args[j + 1:]
                    break
                if a in ADD_VALUE_OPTS:
                    j += 1
                elif not a.startswith("-") and not re.match(r"^\d*[<>]", a):
                    positional.append(a)
                j += 1
            idx = 0 if sub == "add" else 1
            if len(positional) <= idx:
                continue
            raw = positional[idx]
            ex = expand(raw, env)
            found.append((raw, git_dir, None if ex is None else os.path.normpath(os.path.join(git_dir, ex))))
        return found
    
    
    def repo_root(toplevel):
        """The main checkout's root, from it or any worktree under .claude/worktrees/."""
        marker = os.sep + ALLOWED_DIR + os.sep
        at = toplevel.find(marker)
        return toplevel if at == -1 else toplevel[:at]
    
    
    def check(command, cwd, root_of, env):
        """None when allowed, else the deny reason. root_of(dir) -> repo root or None."""
        if not re.search(r"\bworktree\b", command):
            return None
        for raw, git_dir, resolved in worktree_targets(command, cwd, env):
            repo = root_of(git_dir) or root_of(cwd)
            if not repo:
                continue  # not a repository: git itself will refuse
            allowed = os.path.join(repo, ALLOWED_DIR)
            if resolved and resolved.startswith(allowed + os.sep):
                continue
            where = ("resolves to " + resolved) if resolved else "cannot be resolved before it runs"
            return (
                'Blocked: worktree path "%s" %s, outside %s/.\n'
                "Worktrees live only under .claude/worktrees/ so cleanup-wt and Claude Code's sweep can see them.\n"
                'Prefer the Agent tool\'s isolation: "worktree" or `claude --worktree <name>`. If you need git directly:\n'
                "  git worktree add .claude/worktrees/<name> -b <branch> <base>\n"
                "(from the repo root, with a literal path)." % (raw, where, allowed)
            )
        return None
    
    
    def toplevel_of(d):
        env = {k: v for k, v in os.environ.items() if not k.startswith("GIT_")}
    
  • hooks/pre-commit-readme.shGitHub

All 7 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.

@erictechpro
Ships withsuper-board

Autonomous GitHub Project build → QA → review loop for Claude Code. One-line install.

Stars on GitHub

Momentum

stars in total
670
Star history of EricTechPro/super-board on GitHub, a chart by star-history.com

More plugins that ship hooks.

oh-my-claudecode

oh-my-claudecode

Splits a complex task across several specialized Claude Code agents working in parallel, then keeps going until the result is checked. You describe what you want in plain language instead of memorizing commands.

84 skills+441k
@yeachan-heo@yeachan-heo
Install Plugin
ouroboros

ouroboros

Turns a rough feature idea into a written spec through guided questions, then has the agent build it and checks the result against that spec. Instead of hand-checking whether the work is finished, you get an automatic evaluation gate that runs in your coding agent.

26 skills+36.4k
@q00@q00
Install Plugin
myclaude

myclaude

A plugin that runs coding tasks through multiple AI backends (Codex, Claude, Gemini, OpenCode) with a structured development workflow, parallel execution, browser automation and multi-session task checkpointing.

11 skills+32.8k
@stellarlinkco@stellarlinkco
Install Plugin
harmonist

harmonist

Coordinates AI agents with mechanical protocol enforcement, a workflow gate, memory validation and a session hook. Includes a catalog of specialist agents and has no runtime dependencies.

199 agents+12.2k
@gammalabtechnologies@gammalabtechnologies
Install Plugin
bernstein

bernstein

Bernstein is a Claude Code plugin for orchestrating AI agents. It adds skills to list and inspect agents, review and approve tasks, create tasks, track costs and budgets, and show alerts for failed or stalled work.

26 skills+41.2k
@sipyourdrink-ltd@sipyourdrink-ltd
Install Plugin
fablize

fablize

A Claude Code plugin that enforces finishing multi-step tasks with evidence and verification at each stage. It works as a procedure for Opus or any Claude model, not as chance.

1 skill+2891
@fivetaku@fivetaku
Install Plugin
hello2cc

hello2cc

Adjusts how third-party models behave inside Claude Code so they run closer to a native Opus session, covering subagent lifecycle, session hooks and tool interception. It does not replace your gateway, provider mapping or account setup.

1 agent11 hooks690
@hellowind777@hellowind777
Install Plugin
bkit

bkit

Runs a PDCA workflow with phase gating, spec checks, and delegation to sub-agents for planning, QA, and deployment. Fires at session start and on prompts, edits, task creation, and phase transitions.

44 skills+3605
@ww-w-ai@ww-w-ai
Install Plugin

Questions about the hooks in super-board.

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