feat: implement git-in-hg blocking hook (passes basic cases)
Author:
Chris Tusa <chris.tusa@leafscale.com>
Date:
May 03, 2026 13:13
Changeset:
2449115e2d9b0fd9b170ae6cd47d5778aac61fad
Branch:
default
Changed files:
Diff
diff -r 2c2381f618b2 -r 2449115e2d9b hooks/block-git-in-hg.sh --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hooks/block-git-in-hg.sh Sun May 03 13:13:22 2026 -0500 @@ -0,0 +1,99 @@ +#!/usr/bin/env bash +# PreToolUse hook: blocks `git` invocations when the working directory +# is inside a Mercurial (.hg) repository. +# +# Reads tool-call JSON from stdin, exits 0 to allow, exits 2 with a +# message on stderr to block. + +set -u + +# Fail open if jq is missing — better to allow a command than to silently +# block all bash for a missing dependency. +if ! command -v jq >/dev/null 2>&1; then + echo "isurus-hg hook: 'jq' not found; allowing command. Install jq to enable git-blocking." >&2 + exit 0 +fi + +input=$(cat) +cmd=$(jq -r '.tool_input.command // ""' <<<"$input") +cwd=$(jq -r '.cwd // ""' <<<"$input") + +# Allow if no command extracted. +if [[ -z "$cmd" ]]; then + exit 0 +fi + +# Strip leading whitespace and any leading VAR=value env assignments. +trimmed=$(sed -E 's/^[[:space:]]+//' <<<"$cmd") +while [[ "$trimmed" =~ ^[A-Za-z_][A-Za-z0-9_]*=[^[:space:]]*[[:space:]]+ ]]; do + trimmed=$(sed -E 's/^[A-Za-z_][A-Za-z0-9_]*=[^[:space:]]*[[:space:]]+//' <<<"$trimmed") +done + +# Extract the leading word. +first=$(awk '{print $1}' <<<"$trimmed") +if [[ "$first" != "git" ]]; then + exit 0 +fi + +# Find VCS context for cwd: the closest .hg or .git ancestor. +# If there's no usable cwd, allow. +if [[ -z "$cwd" || ! -d "$cwd" ]]; then + exit 0 +fi + +dir=$cwd +hg_root="" +git_root="" +while [[ "$dir" != "/" && -n "$dir" ]]; do + if [[ -z "$hg_root" && -d "$dir/.hg" ]]; then + hg_root=$dir + fi + if [[ -z "$git_root" && -d "$dir/.git" ]]; then + git_root=$dir + fi + if [[ -n "$hg_root" || -n "$git_root" ]]; then + break + fi + dir=$(dirname "$dir") +done + +# If a .git was found anywhere up the walk, allow. The loop breaks at +# the first iteration where either is found, so a non-empty git_root means +# .git is at the same level as .hg or closer — either way, the user is in +# a git context. +if [[ -n "$git_root" ]]; then + exit 0 +fi + +# No .hg ancestor => allow (e.g. plain `git --version` outside any repo). +if [[ -z "$hg_root" ]]; then + exit 0 +fi + +# We are inside an .hg repo and the command starts with `git`. Block. +# Build a suggestion based on the next word. +sub=$(awk '{print $2}' <<<"$trimmed") +case "$sub" in + status) suggest="hg status" ;; + add) suggest="hg add" ;; + commit) suggest="hg commit -m \"...\" (no -u; identity comes from ~/.hgrc)" ;; + log) suggest="hg log" ;; + diff) suggest="hg diff" ;; + branch) suggest="hg bookmarks (in this workflow, bookmarks are feature branches)" ;; + checkout) suggest="hg update <name>" ;; + pull) suggest="hg pull -u" ;; + push) suggest="hg push -B <bookmark>" ;; + blame) suggest="hg annotate" ;; + *) suggest="See the mercurial-basics skill for the full command map." ;; +esac + +cat >&2 <<EOF +git command blocked: this is a Mercurial repository (.hg/ found at $hg_root). + +You ran: $trimmed +Use: $suggest + +See the mercurial-basics skill for the full command map. +EOF + +exit 2