2026-06-29 11:41:41 -04:00
#!/usr/bin/env bash
# install-hooks.sh — install the Sea Haven security-review git hooks + skill assets.
#
# Two modes:
# install-hooks.sh --global Lay the hooks down once for EVERY repo on this machine:
# writes ~/.config/git/hooks/{pre-commit,pre-push}, sets
# git config --global core.hooksPath, and links the skill
# prompt + finding.schema.json into ~/.claude (Path A).
# install-hooks.sh /path/to/repo Per-repo install: copy the hooks into <repo>/.git/hooks
# (use when a repo sets its own local core.hooksPath, e.g.
# husky, which would otherwise shadow the global hook).
# install-hooks.sh --help
#
# The hooks run review.sh --scanners-only (fast, deterministic). The full agentic review is the
# on-demand /sh-security-review skill; the nightly VM sweep is the backstop. Idempotent + re-runnable.
set -euo pipefail
SRC = " $( cd " $( dirname " $0 " ) " && pwd ) "
GLOBAL_HOOKS = " $HOME /.config/git/hooks "
CLAUDE_DIR = " $HOME /.claude "
2026-07-14 19:24:01 -04:00
SUP_DIR = " ${ SH_SECURITY_SUPPRESSIONS_DIR :- $HOME /.config/sea-haven/security-review } "
2026-06-29 11:41:41 -04:00
usage( ) { grep '^#' " $0 " | sed 's/^# \{0,1\}//' ; }
install_one( ) { # install_one <src-hook> <dest-hook>
local src = " $1 " dest = " $2 "
cp " $src " " $dest "
chmod +x " $dest "
}
link_asset( ) { # link_asset <src-file> <dest-path> (symlink so the repo stays source of truth)
local src = " $1 " dest = " $2 "
mkdir -p " $( dirname " $dest " ) "
ln -sfn " $src " " $dest "
echo " linked $dest -> $src "
}
case " ${ 1 :- } " in
-h| --help| "" ) usage; exit 0; ;
--global)
echo "== Installing Sea Haven security-review hooks globally =="
mkdir -p " $GLOBAL_HOOKS "
2026-07-14 19:24:01 -04:00
mkdir -p " $SUP_DIR "
echo " suppressions: machine-level per-repo file $SUP_DIR /<repo-basename>/suppressions.json is preferred by the hooks over repo-local .security-review/suppressions.json "
echo " suppressions: review.sh merges both locations when run without --suppressions"
2026-06-29 11:41:41 -04:00
# Warn (don't clobber silently) if a different global hooksPath is already set.
CURRENT = " $( git config --global --get core.hooksPath || true ) "
if [ -n " $CURRENT " ] && [ " $CURRENT " != " $GLOBAL_HOOKS " ] ; then
echo " WARNING: git config --global core.hooksPath is already ' $CURRENT '. " >& 2
echo " Overwriting it with ' $GLOBAL_HOOKS '. Re-point manually if that was intentional. " >& 2
fi
install_one " $SRC /hooks/pre-commit " " $GLOBAL_HOOKS /pre-commit "
install_one " $SRC /hooks/pre-push " " $GLOBAL_HOOKS /pre-push "
git config --global core.hooksPath " $GLOBAL_HOOKS "
echo " installed pre-commit + pre-push -> $GLOBAL_HOOKS "
echo " set git config --global core.hooksPath = $GLOBAL_HOOKS "
# Path A assets: link the on-demand skill prompt + finding schema into ~/.claude.
link_asset " $SRC /skill/sh-security-review.md " " $CLAUDE_DIR /commands/sh-security-review.md "
link_asset " $SRC /finding.schema.json " " $CLAUDE_DIR /security-review/finding.schema.json "
echo
echo "Done. Every repo on this machine is now gated by review.sh --scanners-only before push."
echo "Caveats: a repo that sets its OWN local core.hooksPath (e.g. husky) overrides this global hook"
echo " — run 'install-hooks.sh <that-repo>' to gate it per-repo. Skip a repo with a"
echo " .security-review-skip file at its root; bypass once with 'git push --no-verify'."
; ;
--*)
echo " unknown option: $1 " >& 2; usage; exit 2; ;
*)
REPO = " $1 "
[ -d " $REPO /.git " ] || { echo " not a git repo: $REPO " >& 2; exit 1; }
echo " == Installing security-review hooks into $REPO /.git/hooks == "
for h in pre-commit pre-push; do
DEST = " $REPO /.git/hooks/ $h "
[ -f " $DEST " ] && echo " warning: existing $h hook at $DEST will be overwritten " >& 2
install_one " $SRC /hooks/ $h " " $DEST "
echo " installed $h -> $DEST "
done
echo "note: hooks run deterministic scanners only; full review is /sh-security-review (on demand)."
; ;
esac