Back to blog
EngineeringAug 10, 2026· min read

Fail Closed: When Git Hooks Can't See What You're Committing

Our pre-commit guard was silently passing commits when it couldn't read the git index. Here's how we fixed it to fail closed instead of failing open.

I shipped a one-line change to our pre-commit hook today that would have prevented a catastrophic failure mode I didn't realize we had.

We have a pre-commit guard that blocks commits containing settings.local.json changes from reaching the remote. This file holds API keys, local database credentials, and other secrets that should never leave your machine. The guard is simple: pipe git diff --cached into grep, look for the forbidden filename, and reject the commit if found.

The Problem

The original implementation piped git diff --cached straight into grep without checking whether git diff succeeded. If the git index was corrupted, locked, or otherwise unreadable, git diff would fail silently and grep would see an empty input stream. Empty input means no match. No match means the commit passes.

This is a classic fail-open bug. When the guard can't see what you're committing, it waves you through. The very circumstance that should raise the highest alarm—inability to inspect the commit—became the circumstance that bypassed inspection entirely.

The Fix

The fix is one line of shell scripting discipline: set -o pipefail at the top of the pre-commit hook. This tells the shell to propagate failures from any command in a pipeline, not just the final one. Now if git diff --cached fails, the entire pipeline fails, and the commit is rejected.

I synced this fix from strug-standards PR #41, where we originally caught and fixed it. Our pattern is to build standards in one repository, validate them in production, then sync them across all active repositories. This is the infrastructure-as-code version of eating your own dog food.

Why It Matters

Security guards should fail closed. If you can't inspect the thing you're guarding, the correct behavior is to reject it, not to pass it. This principle extends far beyond git hooks—authentication middleware, API gateways, access control layers, content filters. Any system that enforces a boundary must decide what to do when it can't see what's crossing that boundary.

The default behavior of Unix pipelines is fail-open. Bash doesn't care if the first command in a pipeline fails as long as the last one succeeds. This is reasonable for data processing scripts. It's catastrophic for security guards. Knowing when to override the default is the difference between infrastructure that protects you and infrastructure that lies to you about protecting you.

What's Next

This fix is now synced across strug-enterprise-site. We'll continue propagating it to other repositories as they come online. Longer term, we're building a test suite for our git hooks themselves—unit tests that verify fail-closed behavior when git commands return non-zero exit codes.

The broader lesson is that infrastructure scripts need the same testing discipline as application code. A bash script that guards your secrets is just as critical as the API endpoint that serves them. We're extending our TDD audit test pattern to cover pre-commit hooks, CI pipelines, and deployment scripts. If it enforces a boundary, it gets a test that verifies fail-closed behavior.