Local hooks (the kind in git hooks for Laravel projects) run on the developer’s machine, which means they run only when the developer kept them installed. Server-side hooks run on every push, for everyone, no exceptions. When you self-host, they are one of the best reasons to - and one of the easiest ways to annoy your team if you overreach.
Three checks that earn their place#
1. Block the secrets. A pre-receive hook scanning pushed commits for private keys, .env contents and common token shapes turns a secret got committed from an incident into a rejected push. Gitleaks or a few good regexes; the point is that the secret never lands in the repo’s history at all, which is when cleanup is free.
2. Protect the default branch. Even with branch protection rules configured in the UI, a pre-receive or update hook rejecting direct pushes to main is the belt to the platform’s suspenders - it still fires if someone widens a permission by mistake.
3. Refuse giant blobs. A 200MB binary committed once lives in every clone forever. Rejecting pushes with files over a sane limit (100MB is GitHub’s line; yours can be lower) pushes large assets to Git LFS before they become permanent.
The restraint that keeps them tolerable#
A rejected push interrupts someone mid-flow, so every hook owes them three things:
- A fast answer. A hook that scans ten thousand commits on every push will be bypassed, not loved. Check the pushed range, not all of history.
- A message that says how to fix it. “Push rejected: possible secret in src/config/legacy.php (line 41). Remove it, rewrite the commit, see .” A bare “rejected” teaches people to fear the server; a clear one teaches them the rule.
- A real escape hatch. False positives happen - a test fixture that looks like a key, a legitimate big file. Someone on the team should be able to bypass with a reason, and that bypass should be logged, not forbidden.
Start with the secret scanner; it has the best ratio of one-time setup to incidents prevented. Add rules the way you add tests: when the failure actually happens, not in anticipation of every imaginable one.
Share this post
Keep reading
Backing Up Your Git Server So It Actually Restores
Every clone is a partial backup - but hooks, config and server state live outside the repo. Mirror, bundle, copy off-site, and rehearse the restore.
Monitoring Your Self-Hosted Git Server: The Four Things That Matter
A Git server fails quietly: disks fill, backups rot, SSH dies at 2am. The four checks that catch every realistic failure, and how to keep them boring.
Moving Your Repositories to a New Git Server Without Losing Anything
Git makes migration nearly lossless - every clone carries the full history. The mirror-push recipe, what does not travel with the repo, and the order to do things in.