Skip to content
Development
Hook

Hooks

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

From plugin
vibecheck
41 skill1 hook
Install
> /plugin marketplace add dalvgit/vibecheck
> /plugin install vibecheck@vibecheck

Ships with vibecheck. Installing the plugin gets these hooks.

What fires, and when

PreToolUse

  • MatchesBash"${CLAUDE_PLUGIN_ROOT}/hooks/gate.sh"
Read hooks/hooks.json

Where it lives

  • hooks/gate.shRunsGitHub
    Read the script
    #!/bin/sh
    # vibecheck — the gate.
    #
    # Three modes:
    #   (no args)  PreToolUse hook. Reads the hook JSON on stdin. Exit 2 blocks the push.
    #   --diff     Print the outgoing diff. The exam reads this.
    #   --pass     Record a receipt for the current outgoing diff. The exam calls this after you pass.
    #
    # The receipt is a hash of the diff you were examined on. The gate lets a push
    # through only when that hash still matches what you're about to send. Add a
    # commit and the hash moves, so you sit the exam again.
    
    set -eu
    
    EMPTY_TREE=4b825dc642cb6eb9a060e54bf8d69288fbee4904
    RECEIPT=""
    
    # Our own absolute path — resolved before any cd, so we can hand it to the exam.
    SELF=$(cd "$(dirname "$0")" && pwd)/$(basename "$0")
    
    # Anchor to the repo root. Run from a subdirectory, a relative receipt path is
    # never found (you'd pass the exam and stay blocked forever) and `git diff -- .`
    # would only see the part of the push under your cwd. Returns 1 if not a repo.
    # `--absolute-git-dir`, not a literal .git/, because in a worktree or submodule
    # .git is a file pointing elsewhere.
    anchor() {
      root=$(git rev-parse --show-toplevel 2>/dev/null) || return 1
      cd "$root" || return 1
      gd="$(git rev-parse --absolute-git-dir)"
      RECEIPT="$gd/vibecheck-receipt"
      CHALLENGE="$gd/vibecheck-challenge"
    }
    
    # --- config (all optional; absent file means defaults) ------------------------
    # Only the two keys that decide *whether to examine at all* are read here. The
    # rest of .vibecheck.toml is read by the exam skill itself.
    config() { sed -n "s/^[[:space:]]*$1[[:space:]]*=[[:space:]]*//p" .vibecheck.toml 2>/dev/null | head -1; }
    
    # Defaults to 1: if it isn't an ignored file, you get examined on it.
    # Size is not a proxy for risk — `def is_admin(u): return True` is two lines. The
    # ignore globs are the right filter for "don't bother me", not a line threshold.
    min_lines() { l=$(config min_lines | tr -cd '0-9'); echo "${l:-1}"; }
    
    # Ignore globs become git pathspecs, so ignored files never reach the exam.
    excludes() {
      raw=$(config ignore | tr -d '[]"' )
      [ -n "${raw:-}" ] || raw='*.lock, dist/**, *.md, package-lock.json'
      echo "$raw" | tr ',' '\n' | while read -r g; do
        g=$(echo "$g" | xargs 2>/dev/null || true)
        [ -n "$g" ] && printf ':(exclude,glob)%s\n' "$g"
      done
    }
    
    # --- the outgoing diff --------------------------------------------------------
    # What `git push` would actually send: everything on HEAD since it forked from
    # the branch we're tracking. `A...HEAD` diffs from the merge-base, so a stale
    # upstream doesn't drag unrelated changes into the exam.
    base() {
      if up=$(git rev-parse --abbrev-ref --symbolic-full-name '@{u}' 2>/dev/null); then
        git merge-base "$up" HEAD 2>/dev/null && return 0
      fi
      # No upstream yet — this branch has never been pushed. Fork point from the
      # remote's default branch, falling back through the usual suspects. Skip the
      # branch we're standing on: merge-base(main, HEAD) while *on* main is HEAD
      # itself, which would diff HEAD against HEAD and wave the whole push through.
      here=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "")
      for ref in origin/HEAD origin/main origin/master main master; do
        [ "$ref" = "$here" ] && continue
        if git rev-parse --verify --quiet "$ref" >/dev/null 2>&1; then
          git merge-base "$ref" HEAD 2>/dev/null && return 0
        fi
      done
      echo "$EMPTY_TREE"  # never pushed anywhere: all of it is new
    }
    
    outgoing() {
      # shellcheck disable=SC2046
      git diff "$(base)" HEAD -- . $(excludes) 2>/dev/null || true
    }
    
    hash_outgoing() {
      outgoing | { shasum -a 256 2>/dev/null || sha256sum; } | cut -d' ' -f1
    }
    
    # The receipt: line 1 is the hash of the diff you were examined on, line 2 marks an
    # exam that was skipped rather than passed. It is valid only for the exact diff it
    # was issued against — add a commit and the hash moves, so you sit a new exam.
    #
    # This is a forcing function, not a security control. The hash is derivable from the
    # repo, so a model with a shell could forge the receipt and skip the exam. It doesn't,
    # because in a session you are watching, you would see it happen: no questions, just a
    # push. The human in the loop is the enforcement. See the README on what that means
    # for unattended runs.
    receipt_ok() {
      [ -f "$RECEIPT" ] || return 1
      [ "$(head -1 "$RECEIPT")" = "$(hash_outgoing)" ] || return 1
    }
    
    unlock() { # $1 = "SKIPPED" or empty
      d=$(outgoing)
      [ -n "$d" ] || { echo "vibecheck: nothing outgoing, no receipt issued." >&2; exit 1; }
      printf '%s\n%s\n' "$(hash_outgoing)" "${1:-}" > "$RECEIPT"
    }
    
    # --- modes --------------------------------------------------------------------
    case "${1:-}" in
      --guide)
        # Print the shared, tool-neutral exam guide. Ships next to this script; every
        # adapter (Claude skill, OpenCode plugin, Codex hook) points the model here so
        # they all get the same question-craft. No repo needed.
        d=$(dirname "$SELF")
        for g in "$d/EXAM-GUIDE.md" "$d/../EXAM-GUIDE.md"; do
          [ -f "$g" ] && { cat "$g"; exit 0; }
        done
        echo "vibecheck: EXAM-GUIDE.md not found next to the gate." >&2; exit 1 ;;
      --diff)
        anchor || { echo "vibecheck: not a git repository." >&2; exit 1; }
        outgoing; exit 0 ;;
      --pass)
        anchor || { echo "vibecheck: not a git repository." >&2; exit 1; }
        unlock ""
        echo "🧪 vibecheck passed. Push unlocked."
        exit 0 ;;
      --skip)
        anchor || { echo "vibecheck: not a git repository." >&2; exit 1; }
        unlock "SKIPPED"
        echo "🧪 vibecheck SKIPPED — no exam taken. Pushing code nobody has been checked on."
        exit 0 ;;
    esac
    
    # --- hook mode ----------------------------------------------------------------
    payload=$(cat)
    
    # Pull a string field out of the hook payload. jq if it's there, python3 if not,
    # and failing both, sed — because a gate that quietly stops gating on a machine
    # without jq installed is worse than no gate at all.
    #
    # The sed path: cut everything up to the key, protect escaped quotes behind a
    # sentinel, cut at the closing quot

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 withvibecheck

Your AI wrote it. Your name's on it. Now prove you read it. vibecheck blocks git push until you pass a quick multiple-choice quiz on the diff your coding assistant is shipping. Pass and it goes through. Not in the mood? Skip — it's always offered, no nagging.

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

Repo: dalvgit/vibecheck