# verify-before-commit: an agent hook by Scalably

Canonical: https://scalably.io/hooks/verify-before-commit
Source: https://github.com/scalably-io/agent-hooks/blob/v1.0.0/hooks/verify-before-commit.sh
Runs on: PreToolUse on Bash
Release: v1.0.0 at commit 53dcece
Integrity: sha256 of the script served at https://scalably.io/hooks/verify-before-commit.sh is d15176a4373a83e7e58407ab1df05f6b7fe79ee1e7d0689e68709a1f68af60af. Verify: curl -s https://scalably.io/hooks/verify-before-commit.sh | shasum -a 256
This is the machine-readable representation of the page at the canonical URL. Same facts, denser format.

## Summary

PreToolUse (Bash) hook: run a project's own verification before commit or push.

## Install

```
/plugin marketplace add scalably-io/agent-skills
/plugin install agent-hooks@scalably-agent-skills
```

## What it does

WHY THERE IS AN ALLOWLIST. This hook executes a script that the REPOSITORY
supplies, at .claude/verify.sh. On a machine where you wrote every repository
that is fine. It is not fine by default: clone a hostile repository that ships
its own verify script, commit once, and you have run the author's code. The
attacker supplies the payload, so presence of the file cannot be the trigger.

Therefore: this hook runs nothing until you have allowlisted that repository
by absolute path, once, in ~/.claude/verify-allowlist (override with
VERIFY_ALLOWLIST). Entries are exact paths. A parent directory does not
allowlist the repositories beneath it.

```text
  printf '%s\n' "$(git rev-parse --show-toplevel)" >> ~/.claude/verify-allowlist
```

What goes in .claude/verify.sh is yours: tests, lint, typecheck, build, a smoke
script. Whatever "correct" means for that project.

Exit 0 = allow (not a commit or push, repository not allowlisted, no verify
```text
         script, or verification passed)
```

Exit 2 = block, stderr is shown to the model as the reason

## The whole script

```bash
#!/usr/bin/env bash
# PreToolUse (Bash) hook: run a project's own verification before commit or push.
#
# WHY THERE IS AN ALLOWLIST. This hook executes a script that the REPOSITORY
# supplies, at .claude/verify.sh. On a machine where you wrote every repository
# that is fine. It is not fine by default: clone a hostile repository that ships
# its own verify script, commit once, and you have run the author's code. The
# attacker supplies the payload, so presence of the file cannot be the trigger.
#
# Therefore: this hook runs nothing until you have allowlisted that repository
# by absolute path, once, in ~/.claude/verify-allowlist (override with
# VERIFY_ALLOWLIST). Entries are exact paths. A parent directory does not
# allowlist the repositories beneath it.
#
#   printf '%s\n' "$(git rev-parse --show-toplevel)" >> ~/.claude/verify-allowlist
#
# What goes in .claude/verify.sh is yours: tests, lint, typecheck, build, a smoke
# script. Whatever "correct" means for that project.
#
# Exit 0 = allow (not a commit or push, repository not allowlisted, no verify
#          script, or verification passed)
# Exit 2 = block, stderr is shown to the model as the reason

input=$(cat)
command=$(printf '%s' "$input" | jq -r '.tool_input.command // ""' 2>/dev/null) || exit 0
cwd=$(printf '%s' "$input" | jq -r '.cwd // ""' 2>/dev/null) || exit 0

# Only fire on commit or push.
printf '%s' "$command" | grep -qE '\bgit[[:space:]]+(commit|push)\b' || exit 0

[ -n "$cwd" ] && [ -d "$cwd" ] || exit 0
cd "$cwd" 2>/dev/null || exit 0
root=$(git rev-parse --show-toplevel 2>/dev/null) || exit 0
root=$(cd "$root" 2>/dev/null && pwd -P) || exit 0

verify="$root/.claude/verify.sh"
[ -x "$verify" ] || exit 0

# The security gate. Exact path match only, so a listed parent never implies a
# child. Checked BEFORE the script is executed, and nothing above this line runs
# repository-supplied code.
allowlist="${VERIFY_ALLOWLIST:-$HOME/.claude/verify-allowlist}"
allowed=0
if [ -f "$allowlist" ]; then
  while IFS= read -r line; do
    case "$line" in ''|'#'*) continue ;; esac
    entry=$(cd "$line" 2>/dev/null && pwd -P) || entry="$line"
    [ "$entry" = "$root" ] && { allowed=1; break; }
  done < "$allowlist"
fi

if [ "$allowed" -ne 1 ]; then
  {
    echo "agent-hooks: $root has a .claude/verify.sh but is not allowlisted, so it was not run."
    echo "This hook executes a script the repository supplies. Allowlist it only if you trust this repository:"
    echo "  printf '%s\\n' \"$root\" >> ${allowlist}"
  } >&2
  exit 0
fi

# Portable timeout: macOS ships no timeout binary, so use a watchdog.
tmpout=$(mktemp) || exit 0
( cd "$root" && ./.claude/verify.sh >"$tmpout" 2>&1 ) &
pid=$!
elapsed=0
while kill -0 "$pid" 2>/dev/null && [ "$elapsed" -lt 300 ]; do
  sleep 1; elapsed=$((elapsed + 1))
done
if kill -0 "$pid" 2>/dev/null; then
  kill -9 "$pid" 2>/dev/null
  code=124
else
  wait "$pid"; code=$?
fi
out=$(cat "$tmpout" 2>/dev/null); rm -f "$tmpout"

[ "$code" -eq 0 ] && exit 0

if [ "$code" -eq 124 ]; then
  echo "BLOCKED: .claude/verify.sh timed out after 300s. Run it by hand to see where it hangs." >&2
  exit 2
fi

{
  echo "BLOCKED: $root/.claude/verify.sh failed (exit $code). Fix the cause before committing."
  echo "Do not weaken the check or edit the tests to match the new behaviour."
  echo "--- output (last 40 lines) ---"
  printf '%s\n' "$out" | tail -40
} >&2
exit 2
```
