Back to Blog
Security March 06, 2026 | 9 min read

Pre-Commit Security Gates: 9 Blocking Pillars for Code Quality

Implementing pre-commit hooks that enforce 9 blocking security and quality pillars: grep scanning for String.to_atom, hardcoded secrets, AST analysis mix tasks, and pillar-based enforcement architecture.

Tomas Korcak (korczis)

Prismatic Platform

Shifting Security Left


The most expensive bugs are the ones that reach production. Every stage of the pipeline where you can catch a vulnerability β€” from IDE to pre-commit to CI to staging β€” reduces the cost and risk by an order of magnitude. Pre-commit hooks are the first automated gate, running in milliseconds on every commit attempt.


Our platform enforces 9 blocking pillars at the pre-commit stage. These are not advisory warnings β€” they reject the commit entirely. The developer must fix the violation before the commit can proceed. This eliminates entire categories of vulnerabilities from the codebase.


The 9 Blocking Pillars


Each pillar targets a specific class of vulnerability or quality issue:


PillarEnforcementTargetDetection Speed

|--------|------------|--------|-----------------|

ZEROBlockingString.to_atom, bare rescue, unsafe binary_to_term< 50ms SEALBlockingHardcoded secrets, SQL injection, Code.eval< 50ms PERFBlockinglength() for emptiness, unbounded queries< 50ms HYGIENEBlockingFile size limits, trailing whitespace, merge conflicts< 100ms NMNDBlockingPlaceholders, TODO/FIXME in lib/, stubs< 50ms TACHBlockingMissing test files for changed modules< 200ms DOCSBlockingMissing @moduledoc, @doc, @spec< 500ms DEPSBlockingUnversioned deps, unstable git refs< 100ms RDMEBlockingMissing README.md in umbrella apps< 50ms

Hook Architecture


The pre-commit hook is a shell script that runs each pillar check sequentially. Any single failure blocks the entire commit:



#!/usr/bin/env bash

set -euo pipefail


# Pre-commit hook: 9 blocking pillars

# Runs against staged files only for speed


STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM)

STAGED_EX_FILES=$(echo "$STAGED_FILES" | grep -E '\.ex$' || true)

STAGED_LIB_FILES=$(echo "$STAGED_EX_FILES" | grep '^apps/.*/lib/' || true)

EXIT_CODE=0


# === PILLAR 1: ZERO (Runtime Safety) ===

if [ -n "$STAGED_LIB_FILES" ]; then

# Check for String.to_atom (atom table exhaustion)

if echo "$STAGED_LIB_FILES" | xargs grep -n 'String\.to_atom(' 2>/dev/null | \

grep -v 'String\.to_existing_atom(' | grep -v '# ZERO:OK'; then

echo "ZERO VIOLATION: String.to_atom found. Use String.to_existing_atom instead."

EXIT_CODE=1

fi


# Check for bare rescue (swallowed errors)

if echo "$STAGED_LIB_FILES" | xargs grep -nP 'rescue\s+_\s*->' 2>/dev/null | \

grep -v '# ZERO:TYPED'; then

echo "ZERO VIOLATION: Bare rescue found. Catch specific exception types."

EXIT_CODE=1

fi


# Check for unsafe binary_to_term

if echo "$STAGED_LIB_FILES" | xargs grep -n 'binary_to_term(' 2>/dev/null | \

grep -v '\[:safe\]' | grep -v '# ZERO:OK'; then

echo "ZERO VIOLATION: Unsafe binary_to_term. Use [:safe] option."

EXIT_CODE=1

fi

fi


SEAL Pillar: Secret Detection


The SEAL pillar scans for hardcoded secrets using pattern matching. This catches API keys, passwords, private keys, and cloud credentials before they enter the repository:



defmodule Prismatic.Enforcement.SEAL.SecretScanner do

@moduledoc """

AST-based secret detection for pre-commit and CI enforcement.

Scans Elixir source files for hardcoded credentials and API keys.

"""


@patterns [

{~r/(?:api[_-]?key|secret[_-]?key|password)\s[:=]\s"[^"]{8,}"/, :credential},

{~r/sk-[a-zA-Z0-9]{20,}/, :stripe_key},

{~r/ghp_[a-zA-Z0-9]{36}/, :github_token},

{~r/AKIA[0-9A-Z]{16}/, :aws_access_key},

{~r/-----BEGIN (?:RSA |EC )?PRIVATE KEY-----/, :private_key},

{~r/xox[bporas]-[a-zA-Z0-9-]+/, :slack_token},

{~r/eyJ[a-zA-Z0-9_-]+\.eyJ[a-zA-Z0-9_-]+/, :jwt_token}

]


@spec scan_file(String.t()) :: list(map())

def scan_file(file_path) do

case File.read(file_path) do

{:ok, content} ->

content

|> String.split("\n")

|> Enum.with_index(1)

|> Enum.flat_map(fn {line, line_number} ->

scan_line(line, line_number, file_path)

end)


{:error, _} ->

[]

end

end


defp scan_line(line, line_number, file_path) do

# Skip comments and test fixtures

trimmed = String.trim(line)


if String.starts_with?(trimmed, "#") or String.contains?(line, "test_fixture") do

[]

else

Enum.flat_map(@patterns, fn {pattern, type} ->

if Regex.match?(pattern, line) do

[%{

file: file_path,

line: line_number,

type: type,

severity: :critical,

message: "Potential hardcoded #{type} detected"

}]

else

[]

end

end)

end

end

end


TACH Pillar: Test File Existence


The TACH pillar ensures that every changed module in lib/ has a corresponding test file. This does not verify test content β€” it verifies that the test file exists:



defmodule Prismatic.Enforcement.TACH.TestCoverage do

@moduledoc """

Verifies test file existence for changed source modules.

Maps lib/ paths to test/ paths and checks existence.

"""


@spec check_changed_files(list(String.t())) :: {:ok, list()} | {:violations, list(map())}

def check_changed_files(staged_files) do

violations =

staged_files

|> Enum.filter(&lib_file?/1)

|> Enum.reject(&excluded?/1)

|> Enum.map(fn file -> {file, expected_test_path(file)} end)

|> Enum.reject(fn {_src, test_path} -> File.exists?(test_path) end)

|> Enum.map(fn {src, test_path} ->

%{

source_file: src,

expected_test: test_path,

message: "Missing test file for #{src}"

}

end)


if violations == [] do

{:ok, []}

else

{:violations, violations}

end

end


defp expected_test_path(lib_path) do

lib_path

|> String.replace("/lib/", "/test/")

|> String.replace_suffix(".ex", "_test.exs")

end


defp lib_file?(path), do: String.contains?(path, "/lib/") and String.ends_with?(path, ".ex")


defp excluded?(path) do

exclusions = [

"application.ex",

"repo.ex",

"endpoint.ex",

"telemetry.ex",

"/migrations/",

"/types/"

]


Enum.any?(exclusions, &String.contains?(path, &1))

end

end


PERF Pillar: Performance Anti-Patterns


The PERF pillar catches performance anti-patterns that are correct but inefficient:



# PERF VIOLATIONS β€” caught at pre-commit


# O(n) emptiness check when O(1) is available

if length(list) == 0, do: :empty # BANNED

if list == [], do: :empty # CORRECT


if length(list) > 0, do: :has_items # BANNED

if list != [], do: :has_items # CORRECT


# Unbounded query β€” can return millions of rows

Repo.all(from u in User) # BANNED

Repo.all(from u in User, limit: 100) # CORRECT


# N+1 query pattern

Enum.map(users, fn u -> # BANNED

Repo.get(Profile, u.profile_id)

end)


from(p in Profile, # CORRECT

where: p.user_id in ^user_ids)

|> Repo.all()


Mix Task Enforcement


Beyond grep-based scanning, the pre-commit hook runs mix tasks for deeper analysis:



# === MIX TASK CHECKS ===

# Format check (fast, usually < 1s)

if ! mix format --check-formatted 2>/dev/null; then

echo "FORMAT VIOLATION: Run 'mix format' to fix formatting."

EXIT_CODE=1

fi


# Compilation with warnings-as-errors

if ! mix compile --warnings-as-errors --force 2>/dev/null; then

echo "COMPILE VIOLATION: Fix all compiler warnings."

EXIT_CODE=1

fi


# Truth validation (dynamic claim checking)

if ! mix truth.validate 2>/dev/null; then

echo "TRUTH VIOLATION: Platform claims do not match reality."

EXIT_CODE=1

fi


Measuring Enforcement Effectiveness


We track enforcement metrics to understand what violations developers encounter most frequently and how quickly they resolve them:


MetricQ1 2026 AverageTarget

|--------|----------------|--------|

Pre-commit rejections per developer per week3.2< 5 (learning signal) ZERO violations caught45% of rejectionsDecreasing over time SEAL violations caught12% of rejectionsNear zero (rare) PERF violations caught28% of rejectionsDecreasing over time Average time to fix rejection2.1 minutes< 5 minutes Violations reaching CI (bypassed hook)0.3%0% (impossible with hook)

The pre-commit gate is the foundation of the entire quality pipeline. By catching violations in the developer's terminal before they even enter git history, we maintain a clean codebase where security and quality are structural properties rather than aspirational goals. The 9 blocking pillars have eliminated entire categories of vulnerabilities from the platform, and the grep-based approach ensures that enforcement adds negligible friction to the development workflow.

Tags

pre-commit security elixir ci-cd code-quality

Related Glossary Terms