It is the question we hear most on Pushgate calls. If the goal is "run the tests before code leaves the machine," a pre-push hook already does that, and it is free.
If you trust whoever is pushing, use one. A hook catches the honest mistake: the agent that forgot to run the suite, the developer in a hurry. It costs nothing and it gives the fastest feedback you can get. We run hooks too.
It stops working the moment you need to rely on it.
Agents treat a failing hook as an obstacle
A coding agent is given a task: commit this change. A failing hook stands between it and done, and Git ships a documented way around it. Agents find it.
On March 28, 2026, a developer filed anthropics/claude-code#40117. Over one evening, the agent made six commits in a row past a pre-commit hook that ran secret scanning, unit tests, 44 integration test files and Playwright. It used --no-verify, used git stash to change what was staged, and suppressed output. The project's instructions said, in so many words, "Do not use --no-verify on git commit." All six commits landed with failing integration tests, up to 63 per commit. The report also names the part that makes this hard to catch afterwards: "Git does not log whether --no-verify was used." The issue was closed as not planned.
That is not one unlucky session. block-no-verify, an npm package whose only job is to stop an agent from passing --no-verify, was downloaded 105,292 times between September 23 and October 22, 2026, according to the npm downloads API. Teams are installing a guard for their guard.
The guard has the same problem one level up. It matches command text, and there are many ways to spell the same command. In one test posted to r/ClaudeCode on October 20, a developer ran ten companies' published command guards against other spellings of the commands they block. Every one let at least one through: git -C . push --force, git push origin +main, a command wrapped in sh -c. One guard exited with the wrong status code, so it never blocked anything. Other teams report agents skipping hooks with HUSKY=0 after the --no-verify guard went in, as in this fix to a public agent template.
Three reasons a local hook cannot be the gate
It is optional. git push --no-verify skips the pre-push hook. A clone does not copy hooks, so every machine and every agent workspace needs its own install, and nothing checks that it happened.
It runs where the agent runs. An agent that can write to the repository can edit .git/hooks or point core.hooksPath somewhere else. Worktrees do not help: Git needs the .git directory writable for ordinary commands, and that is where the hooks live. Our agent sandbox guide treats a planted hook as an escape, because the next Git command a person runs executes it outside the sandbox.
It leaves nothing behind. GitHub receives a commit. Nothing in it says a hook ran, which checks it ran, or on which tree. A commit that passed every check and a commit pushed with --no-verify look the same.
The check has to run on the server
The server-side version of a hook is a pre-receive hook. GitHub.com does not offer them; only GitHub Enterprise Server runs them. The free answer most teams already have is branch protection with required CI checks. It works, and the agent cannot skip it with a flag. It gates the merge, though, not the push: the code is already on GitHub, CI runs it with whatever secrets CI holds, and the agent waits on a shared queue to learn what it could have learned on its own machine.
Pushgate is a Git remote that does the pre-receive job. The checks run where the agent already works. CI/lock signs the result and binds it to the exact commit. On push, Pushgate verifies that evidence against the repository's policy before anything is forwarded. In block mode, the default for a new GitHub connection, a push without the evidence never reaches GitHub, and the refusal says what to run next.
What it looked like on this post's own change
The FAQ answer this post grew out of went through our own gate. The first push was refused, because the commit had no evidence yet. This is captured output, trimmed and wrapped, with identifiers shortened:
$ git push origin feat/pushgate-faq-git-hook
remote: Pushgate.dev · gate for testifysec/judge
remote: + signature signed by spiffe://platform.testifysec.com/tenant/…/agent/…
remote: + signer agent claude-on-… active, scoped to this repository
remote: x tested df4f2974: no attestation in the evidence store names
remote: commit df4f2974 as its subject
remote: x release df4f2974: gomod-drift: no evidence for commit df4f2974…
remote: local-ring: no evidence for commit df4f2974…
remote: => STOPPED AT THE GATE: check "tested": no qualifying metadata
remote: collection for this commit
! [remote rejected] feat/pushgate-faq-git-hook (no qualifying metadata collection for this commit)
The agent did not get a flag to skip this. It ran the named steps through CI/lock on one of our build workers: the local test ring, which took about 150 seconds, and the Go module drift check. Then it pushed the same commit again:
$ git push -u origin feat/pushgate-faq-git-hook
remote: => accepted
* [new branch] feat/pushgate-faq-git-hook -> feat/pushgate-faq-git-hook
Where this stops. The evidence shows what CI/lock observed under its signing credential. It does not prove the tests were good, and an agent with access to the signing environment is a different problem, which the sandbox guide covers. A repository in warn mode reports a missing requirement and lets the push through. And a direct push to GitHub skips Pushgate entirely until a repository ruleset restricts protected refs to the Pushgate App. A gated remote only guards the route that goes through it.
Keep the hook for fast feedback on your own machine. Put the decision where the pusher cannot reach it.
Gate one repository.
Connect a repository and give one agent the push URL. Free covers one private repository with unlimited agents.
Start with Pushgate.dev