Back to blog
EngineeringDate unavailable· min read

Failing Closed: Making Pre-Commit Hooks Safer

Sometimes the most important fixes are the ones that prevent failures you haven't seen yet. Today we shipped a small but critical update to our pre-commit hook infrastructure: making the settings.local.json guard fail closed when it can't read the git index.

The Problem: Silent Pipeline Failures

Our pre-commit hook protects against accidentally committing settings.local.json—a file that should always stay local because it contains developer-specific configuration. The original implementation piped git diff --cached straight into grep:

git diff --cached --name-only | grep settings.local.json

This works fine 99% of the time. But what happens when git diff --cached fails to read the index? Maybe the index is corrupted, or there's a permission issue, or git itself hits an unexpected error. In those cases, the pipeline would silently succeed—grep would receive no input, find no matches, and the hook would wave the commit through.

That's a security vulnerability hiding in plain sight. If the guard can't inspect the commit, it should block it—not approve it.

The Fix: Fail Closed

The solution is defensive programming 101: check the exit code of git diff --cached before trusting its output. Now the hook explicitly verifies that the index read succeeded. If it fails for any reason, the commit is blocked.

This is fail-closed design: when the system can't determine whether a commit is safe, it assumes it's not. It's the same principle behind authentication timeouts, cert validation errors, and firewall default-deny rules. Uncertainty should always err on the side of safety.

Why This Matters

This fix came from syncing strug-standards PR #41, which means it's part of a broader pattern we're enforcing across our infrastructure. Pre-commit hooks are the first line of defense against configuration leaks, credential exposure, and other commit-time mistakes. Making them resilient to edge cases isn't paranoia—it's due diligence.

The fact that this was synced from strug-standards also means every other repo following the standard will pick up this fix automatically. One defensive improvement propagates everywhere.

What's Next

This fix raises a bigger question: what other pre-commit guards are we running, and do they all fail closed? We should audit the entire .githooks/pre-commit file for similar pipeline fragility patterns. Any place we're piping git commands into filters without checking exit codes is a potential vulnerability.

We should also consider adding test coverage for our git hooks. Pre-commit hooks are code too—they should be tested like any other security boundary. Simulating index read failures, permission errors, and corrupted git state would give us confidence that the guards actually work when things go wrong.

Finally, we might want to add logging or telemetry to hook failures. Right now, if this guard blocks a commit, the developer sees an error message but we don't get visibility into how often it's triggering or why. Instrumentation would help us understand whether this defensive check is catching real issues or just adding friction.