Shifting left means catching defects as early as possible in the development flow — before code even reaches CI. Pre-commit hooks run fast checks (formatting, linting, type-checking, unit tests on changed files, secret scanning) locally at commit time, and static analysis inspects code for bugs and vulnerabilities without executing it.
The earlier a defect is caught, the cheaper it is to fix; a lint error caught at commit costs seconds, the same issue caught in a CI run costs minutes of feedback loop, and in production it can cost far more. Pre-commit hooks (via tools like Husky/lint-staged, or pre-commit for Python) run a curated set of fast checks against staged files before the commit is created, so obvious problems never enter history. Static analysis — linters, type checkers, and security/SAST scanners — reads the code to find likely bugs, code smells, and vulnerable patterns without running it, complementing tests (which verify behavior) by catching issues tests might miss. The balance is speed: pre-commit checks must be fast (seconds) or developers bypass them, so heavier analysis runs in CI while only the quickest, highest-value checks run locally. Crucially, local hooks are a convenience, not a guarantee (they can be skipped), so the same checks must also run as CI gates.
A team adds a pre-commit hook running Prettier, ESLint and a secret scanner on staged files. Formatting debates in code review vanish, obvious lint errors never reach CI, and an accidental API key is blocked at commit time — while CI re-runs the same checks so a developer who skips the hook is still caught.
# package.json with Husky + lint-staged (runs on staged files at commit)
{
"lint-staged": {
"*.{js,jsx,ts,tsx}": ["eslint --max-warnings=0", "prettier --write"],
"*": "gitleaks protect --staged" // secret scan before commit
}
}
# .husky/pre-commit
# npx lint-staged
# CI re-enforces the SAME checks (hooks are skippable with --no-verify):
# - run: npm run lint && npm run type-check
# - run: npx gitleaks detect --source .Scenario: A team relies entirely on pre-commit hooks for linting and secret scanning 'so CI can be lighter'. A contractor commits with --no-verify, bypassing the hooks, and pushes code with lint errors and a leaked token. How do you prevent this while keeping fast local feedback?