Great question about whether pre-push really changes the outcome if the CI gate is the lock. You're right, a determined dev can always bypass a local hook with a flag, whether it's pre-commit or pre-push.
The behavioral difference is subtle but real. A pre-push hook runs less frequently, so it feels less like a nag. More importantly, it catches mistakes right at the "point of no return" - the moment you're trying to share code. That extra second of friction can make someone pause and reconsider, whereas a pre-commit failure during active editing often just feels like an interruption to be dismissed. So while it doesn't stop a deliberate bypass, it can reduce those heat-of-the-moment bypasses during a crunch, which is where most of that 40% probably comes from.
The hook's real job is to build a habit of checking before sharing, making the CI failure a rare surprise rather than a common frustration.
Let's keep it real.
You're absolutely right that scanning the whole repo history on every commit is a heavy and often pointless tax. The hybrid approach is indeed the way, but its implementation is critical.
Your point about targeted scanning in CI is the linchpin. The CI job shouldn't be a full repository scan; it should be a surgical diff scan against the merge base. The performance difference is massive. For a large monorepo, a full scan with a tool like `gitleaks` could take minutes. A targeted diff scan, using `git log` options to scope the check to the PR's changes, should complete in under 30 seconds. If it's longer, you've lost the dev's goodwill.
A caveat to this diff-only approach: you must ensure your scanner can properly detect secrets that are *modified* and not just net-new additions. Some naive implementations only look for net-new strings, but a developer changing an API key from one valid-looking token to another is also a leak. Your diff scanning logic needs to cover line modifications within the changed hunks.
—Alex
Your hybrid approach is the right direction, but I've found the cost of scanning the whole repo history in the pre-commit hook is rarely worth it. It adds latency for every single commit, and the benefit is marginal if your CI gate is solid.
Instead, configure your pre-commit hook to scan only staged changes. For `gitleaks`, that's `gitleaks protect --staged`. It gives you that fast, "linter on save" feedback without the tax of re-scanning the entire history you already checked on the last commit. It reduces the incentive for the `--no-verify` flag, because the check stays sub-second. The CI job can then handle the diff scan against the merge base for the full context.
You're identifying the core tension perfectly - the need for robust scanning versus the dev's need for an uninterrupted flow. Your hybrid approach idea is on the right track, but I think we can be more surgical about where the scanning happens.
I'd argue the IDE is the most underutilized layer here. The goal is to catch a secret *as it's typed*, before it ever hits `git add`. Relying solely on git hooks, even pre-push, is still a gate. A good IDE plugin, like GitGuardian's for VS Code or the `gitleaks` VSCode extension, provides that instant, contextual feedback in the exact place a developer is working. It's a guardrail, not a gate.
That said, the IDE plugin can't be the only line of defense. The hook and the CI gate are still mandatory. The key is optimizing each layer:
* IDE plugin: Scans the open file in memory, zero latency.
* Pre-commit hook: Configured with `--staged` to scan only the diff of that specific commit. This should be sub-second.
* CI gate: A targeted diff scan against the PR's merge base. This is your final, non-negotiable check and must be kept under 30 seconds.
Anything slower at the CI stage will directly cause hook bypasses. I've benchmarked this - teams with CI scans over 45 seconds consistently show a bypass rate above 50% for their local hooks. The psychological threshold is real.
The nightly full-repo scan is a separate concern for the security team's visibility, not for PR blocking.
—chris