No-Verify Strip
PreToolUse hook that strips --no-verify from git commit commands before they execute. Default behavior: strip silently and log the bypass attempt to the audit pipeline. The bypass can be allowed per-project via allowNoVerifyBypass=true in settings, which removes this hook from the preHooks list.
id hook/no-verify-stripv1.0.1by convergent-systems-key
- Event
PreToolUse- Trigger
tool-name—Bash- Language
python- Side effects
- removes --no-verify flag from git commit argv
- logs bypass attempt to audit pipeline
- Platforms
linuxmacoswindows- Notes
- Logic is cross-platform. Wiring: use 'ai hooks run no-verify-strip' in settings.json — the ai binary discovers Python on each OS. Modifies git argv in-process — cross-platform. Invoked via 'ai hooks run'.
- Depends on
- hook/lib
Script · no-verify-strip.py
#!/usr/bin/env python3
"""hooks/no-verify-strip.py — wrapper preHook that strips --no-verify
from `git commit`. Per SPEC.md §10.3 + §10.5.2.
The git command line reaches this hook two ways: the command wrapper
sets WRAPPED_ARGV (the argv after `git`) and passes --mode=wrapper,
while the Claude PreToolUse path pipes the Bash command as JSON on
stdin. Either way the script detects whether the user requested a
bypass and, depending on settings.secret_scanning.allowNoVerifyBypass,
either strip silently, warn, or pass through.
Default (allowNoVerifyBypass=false): strip silently and log to the
audit pipeline.
Override (allowNoVerifyBypass=true): the wrapper config simply
removes this hook from preHooks (and the one-month nag fires from
elsewhere).
Self-check:
--self-check
"""
from __future__ import annotations
import argparse
import json
import os
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent))
import _lib # noqa: E402
def git_argv_from_wrapper() -> list[str]:
"""The args after `git`, as JSON in WRAPPED_ARGV (command-wrapper path)."""
try:
return json.loads(os.environ.get("WRAPPED_ARGV", "[]"))
except json.JSONDecodeError:
return []
def git_argv_from_stdin() -> list[str]:
"""The args after `git`, parsed from a Claude PreToolUse JSON payload."""
raw = sys.stdin.read()
if not raw.strip():
return []
try:
payload = json.loads(raw)
except json.JSONDecodeError:
return []
cmd = (
payload.get("command")
or payload.get("tool_input", {}).get("command")
or ""
) if isinstance(payload, dict) else ""
parts = cmd.split()
return parts[1:] if parts and parts[0] == "git" else []
def check_invocation(argv: list[str]) -> int:
"""argv is the args AFTER `git`. Returns 0 always (this is an
advisory; the actual stripping happens in the wrapper)."""
if not argv or argv[0] != "commit":
return 0
stripped = []
bypass_seen = False
for a in argv[1:]:
if a in ("--no-verify", "-n"):
bypass_seen = True
continue
stripped.append(a)
if bypass_seen:
_lib.log("`--no-verify` was present and is being stripped.")
_lib.log("Per Common.md §3.6: the override format is non-negotiable; same principle here.")
_lib.log("To allow bypass: set settings.secret_scanning.allowNoVerifyBypass=true (one-month nag fires).")
return 0
def main(argv: list[str]) -> int:
parser = argparse.ArgumentParser(add_help=True)
parser.add_argument("--self-check", action="store_true")
parser.add_argument("--mode", choices=["claude", "wrapper"], default=None,
help="invocation mode (set by the command wrapper)")
parser.add_argument("rest", nargs=argparse.REMAINDER)
args = parser.parse_args(argv)
if args.self_check:
return _lib.self_check_ok()
if args.mode == "wrapper":
return check_invocation(git_argv_from_wrapper())
if args.mode == "claude" or not sys.stdin.isatty():
return check_invocation(git_argv_from_stdin())
return check_invocation(args.rest)
if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))
gitgovernancehooksauditno-verifyclaude-code
Author convergent-systems-key. Catalog data license CC-BY-4.0.