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.
Install
> /plugin marketplace add dalvgit/vibecheck > /plugin install vibecheck@vibecheck
Ships with vibecheck. Installing the plugin gets these hooks.
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

