---
id: devsec-pre-commit-security
title: Pre-commit Security — Secret / Lint / 빠른 가드
category: Coding
status: draft
source_trust_level: B
verification_status: conceptual
created_at: 2026-05-09
updated_at: 2026-05-09
tags: [devsecops, pre-commit, hook, vibe-coding]
tech_stack: { language: "Various", applicable_to: ["DevOps"] }
applied_in: []
aliases: [pre-commit, husky, lefthook, lint-staged, gitleaks, secret detection]
---

# Pre-commit Security

> 사고 나기 전에 잡기. **Husky / lefthook / pre-commit framework + lint-staged + gitleaks**. Secret commit 차단 + lint + format. CI 보다 빠른 feedback.

## 📖 핵심 개념
- Pre-commit: commit 전 hook 실행.
- Pre-push: push 전 (더 무거운 검사 가능).
- Lint-staged: 변경 파일만.
- Bypass: --no-verify (긴급).

## 💻 코드 패턴

### Husky (Node 친화)
```bash
yarn add -D husky lint-staged
yarn husky init
```

```jsonc
// package.json
{
  "lint-staged": {
    "*.{ts,tsx}": [
      "eslint --fix",
      "prettier --write"
    ],
    "*.{json,yaml,md}": ["prettier --write"]
  }
}
```

```bash
# .husky/pre-commit
yarn lint-staged
yarn typecheck
```

### Lefthook (Go, 빠름, multi-language)
```yaml
# lefthook.yml
pre-commit:
  parallel: true
  commands:
    lint:
      glob: '*.{ts,tsx}'
      run: yarn eslint --fix {staged_files}
      stage_fixed: true
    
    secrets:
      run: gitleaks protect --staged --no-banner
    
    format:
      glob: '*.{ts,tsx,json,md}'
      run: yarn prettier --write {staged_files}
      stage_fixed: true

pre-push:
  commands:
    typecheck:
      run: yarn tsc --noEmit
    
    test:
      run: yarn test --run

commit-msg:
  commands:
    conventional:
      run: npx commitlint --edit {1}
```

```bash
lefthook install
```

### pre-commit framework (Python, 다양 hook)
```yaml
# .pre-commit-config.yaml
repos:
  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v4.5.0
    hooks:
      - id: trailing-whitespace
      - id: end-of-file-fixer
      - id: check-yaml
      - id: check-added-large-files
        args: [--maxkb=500]
      - id: check-merge-conflict
      - id: detect-private-key
  
  - repo: https://github.com/gitleaks/gitleaks
    rev: v8.18.0
    hooks:
      - id: gitleaks
  
  - repo: https://github.com/returntocorp/semgrep
    rev: v1.45.0
    hooks:
      - id: semgrep
        args: [--config=p/secrets, --error]
  
  - repo: local
    hooks:
      - id: typecheck
        name: TypeScript
        entry: yarn tsc --noEmit
        language: system
        pass_filenames: false
```

```bash
pre-commit install
pre-commit run --all-files  # 한 번 모든 파일
```

### Secret detection (gitleaks)
```toml
# .gitleaks.toml — 자체 rule
[[rules]]
id = "company-api-key"
description = "Company internal API key"
regex = '''cmp_[a-zA-Z0-9]{32}'''

[allowlist]
paths = [
    '''.*\.test\.ts$''',
    '''fixtures/.*''',
]
```

```bash
gitleaks detect --source . --verbose
gitleaks protect --staged --verbose  # pre-commit
```

### Detect-secrets (Yelp)
```bash
# Baseline 생성 (current secrets)
detect-secrets scan --baseline .secrets.baseline

# Pre-commit
detect-secrets-hook --baseline .secrets.baseline $(git diff --cached --name-only)
```

→ Baseline 으로 false positive 관리.

### Commit message — Conventional Commits
```bash
npm install -D @commitlint/cli @commitlint/config-conventional
```

```js
// commitlint.config.js
module.exports = { extends: ['@commitlint/config-conventional'] };
```

```bash
# .husky/commit-msg
npx --no -- commitlint --edit $1
```

→ `feat: xxx` / `fix: yyy` 강제. semantic-release 와 결합.

### TypeScript / lint 빠르게
```bash
# 변경 파일만 lint (lint-staged)
yarn lint-staged

# tsc — incremental
tsc --noEmit --incremental --tsBuildInfoFile .tsbuildinfo
```

### Bypass (긴급, 사용 주의)
```bash
git commit --no-verify -m "WIP"
git push --no-verify
```

→ CI 가 second gate.

### Force re-check (skip 한 후)
```yaml
# CI
- name: Pre-commit
  run: pre-commit run --all-files
```

→ Bypass 해도 PR 에서 잡힘.

### Husky vs lefthook vs pre-commit
```
Husky:
+ Node 친화, 친숙
- Shell script

Lefthook:
+ 매우 빠름 (Go)
+ 병렬
+ Multi-language

pre-commit (Python):
+ 큰 hook ecosystem
+ 매우 강력 framework
- Python 의존
```

### Skip in CI (이미 fixed)
```yaml
- run: SKIP=eslint pre-commit run --all-files
```

### Secret rotate after leak
```bash
# Force push 로 history 삭제 X — secret 가 git history 에 남음.
# 1. Secret 즉시 rotate (모든 곳)
# 2. .git history 안 secret 도 cleanup
git filter-repo --invert-paths --path file-with-secret  # 강력
# 또는 BFG repo-cleaner

# 3. force push (조심) + 모든 사람 re-clone
```

### Large file check
```yaml
- repo: https://github.com/pre-commit/pre-commit-hooks
  hooks:
    - id: check-added-large-files
      args: [--maxkb=500]
```

→ 큰 binary git 안 — Git LFS.

### Private key check
```yaml
- id: detect-private-key
```

→ -----BEGIN RSA PRIVATE KEY----- 자동 검출.

### Branch naming
```bash
# .husky/pre-commit
branch=$(git rev-parse --abbrev-ref HEAD)
if ! [[ "$branch" =~ ^(main|feat|fix|chore)/ ]]; then
  echo "Branch must start with feat/, fix/, or chore/"
  exit 1
fi
```

### Performance (큰 monorepo)
```bash
# Turborepo + pre-commit
yarn lint-staged   # 변경 파일만
turbo run lint --filter='[HEAD^1]'  # 변경 package 만
```

### Onboarding 자동
```bash
# postinstall script
"scripts": {
  "prepare": "husky install"  # auto-install hook
}
```

→ `npm install` 시 hook 자동 setup.

## 🤔 의사결정 기준
| 상황 | 추천 |
|---|---|
| Node only | Husky + lint-staged |
| Multi-language / monorepo | Lefthook |
| Strong ecosystem | pre-commit framework |
| Secret detection | Gitleaks (강) / Detect-secrets (baseline) |
| Conventional commits | commitlint |
| 빠른 feedback | lint-staged + 작은 hook |

## ❌ 안티패턴
- **모든 hook 동기 + 느림**: 사용자가 bypass.
- **--no-verify 자주**: 의미 잃음.
- **모든 file lint 매 commit**: 큰 repo 느림. lint-staged.
- **Secret 발견 후 history 안 cleanup**: 영원 git history 에.
- **Onboarding 수동 hook install**: 새 dev 가 잊음. prepare script.
- **CI 가 hook 안 검사**: bypass = 통과. CI 가 second gate.
- **Format / lint 가 commit 변경**: stage_fixed.

## 🤖 LLM 활용 힌트
- Husky / lefthook + lint-staged + gitleaks 가 baseline.
- Conventional commit + commitlint.
- Bypass 가능 but CI 가 검사.

## 🔗 관련 문서
- [[DevSec_DAST_SAST]]
- [[DevSec_Supply_Chain]]
- [[DevOps_CI_CD_Pipeline_Patterns]]
