Skip to main content

Lefthook Git Hooks 管理

Lefthook 是由 Evil Martians 開發的 Git hooks 管理工具,用單一設定檔就能在 commit、push、merge 等時機自動執行 lint、format、測試等任務。相較 Husky + lint-staged 的組合,Lefthook 原生支援平行執行、檔案過濾、本地覆寫,且不限語言(Go / Ruby / Node / Python 皆可)。

為什麼用 Lefthook?

特性LefthookHusky + lint-staged
設定檔單一 lefthook.yml.husky/ + lint-staged 設定
平行執行內建 parallel: true需額外處理
檔案過濾內建 glob / exclude依賴 lint-staged
跨語言是(單一二進位)主要綁 Node.js 生態
本地覆寫lefthook-local.yml較不便

常見使用情境:

  • pre-commit:只對 staged 檔案跑 eslint、prettier、stylelint
  • pre-push:推送前跑測試或安全掃描
  • post-mergegit pull 後自動 pnpm install
  • commit-msg:驗證 commit message 格式(commitlint)

安裝

依專案環境選擇安裝方式:

# Node.js 專案(最常見)
pnpm add -D lefthook
# 或 npm install lefthook --save-dev

# Homebrew(macOS)
brew install lefthook

# Go
go install github.com/evilmartians/lefthook/v2@latest

安裝後,在專案根目錄執行一次:

npx lefthook install

這會把設定寫入 .git/hooks/,之後每次 git commit 等操作就會觸發對應 hook。

建議在 package.json 加上 prepare script,讓團隊成員 pnpm install 後自動安裝 hooks:

{
"scripts": {
"prepare": "lefthook install"
}
}

基本流程

# 1. 建立或編輯設定檔
vim lefthook.yml

# 2. 安裝 hooks 到 .git/hooks/
lefthook install

# 3. 驗證設定是否正確
lefthook validate

# 4. 手動測試某個 hook(不必真的 commit)
lefthook run pre-commit

# 5. 暫時跳過 lefthook(緊急 commit 時)
LEFTHOOK=0 git commit -m "hotfix"

設定檔結構

設定檔可命名為 lefthook.ymllefthook.yaml.lefthook.yml 等,專案內只保留一份,避免衝突。官方文件:Configuration

Hook 名稱

對應 Git 內建 hooks,例如 pre-commitpre-pushcommit-msgpost-mergepost-checkout

commands 與 jobs

兩種寫法功能相近,新版文件較推 jobs,舊專案常見 commands

# commands 寫法(以名稱為 key)
pre-commit:
commands:
lint:
run: eslint {staged_files}
glob: "*.{js,ts}"

# jobs 寫法(陣列,可加 name、tags)
pre-commit:
jobs:
- name: lint
run: eslint {staged_files}
glob: "*.{js,ts}"

常用欄位

欄位說明
run要執行的 shell 指令
glob只對符合副檔名的 staged 檔案執行
exclude排除特定路徑或 pattern
paralleltrue 時同 hook 內多個任務平行跑
files自訂檔案來源,如 git ls-files -m
script + runner執行腳本檔,如 runner: node
tags標記任務,可用 exclude_tags 在本地略過
stage_fixed自動 git add 被 fix 過的檔案
rchook 執行前載入的 shell script(補 PATH 等)

檔案佔位符

佔位符說明
{staged_files}目前 staged 且符合 glob 的檔案
{all_files}所有符合 glob 的 staged 檔案(含 exclude 邏輯)
{files}搭配自訂 files 指令使用
{push_files}pre-push 時變更的檔案

本地覆寫:lefthook-local.yml

個人開發若不想跑某些 hook,可建立 lefthook-local.yml(建議加入 .gitignore):

# lefthook-local.yml
pre-push:
exclude_tags:
- frontend
jobs:
- name: audit packages
skip: true

也可直接複製 lefthook-local.yml.examplelefthook-local.yml 做自訂 PATH 設定。

入門範例

最小 pre-commit(lint + format)

pre-commit:
parallel: true
jobs:
- run: yarn eslint --fix {staged_files}
glob: "*.{js,ts,jsx,tsx}"
stage_fixed: true

- run: yarn prettier --write {staged_files}
glob: "*.{json,md,yml}"
stage_fixed: true

pre-push 依 tags 分組

pre-push:
jobs:
- name: packages audit
tags:
- frontend
- security
run: yarn audit

- name: gems audit
tags:
- backend
- security
run: bundle audit

多語言 monorepo

pre-commit:
parallel: true
jobs:
- run: yarn eslint {staged_files}
glob: "*.{js,ts,jsx,tsx}"

- name: rubocop
glob: "*.rb"
exclude:
- config/application.rb
- config/routes.rb
run: bundle exec rubocop --force-exclusion {all_files}

- name: govet
files: git ls-files -m
glob: "*.go"
run: go vet {files}

- script: "hello.js"
runner: node

- script: "hello.go"
runner: go run

實戰:Monorepo 前端專案設定

以下是實際在用的 lefthook.yml,以 pnpm monorepo + Vue 為主,commit 前自動格式化與 lint,merge 後自動安裝依賴。

完整設定

# EXAMPLE USAGE:
#
# Refer for explanation to following link:
# https://lefthook.dev/configuration/
#
# pre-push:
# jobs:
# - name: packages audit
# tags:
# - frontend
# - security
# run: yarn audit
#
# - name: gems audit
# tags:
# - backend
# - security
# run: bundle audit
#
# pre-commit:
# parallel: true
# jobs:
# - run: yarn eslint {staged_files}
# glob: "*.{js,ts,jsx,tsx}"
#
# - name: rubocop
# glob: "*.rb"
# exclude:
# - config/application.rb
# - config/routes.rb
# run: bundle exec rubocop --force-exclusion {all_files}
#
# - name: govet
# files: git ls-files -m
# glob: "*.go"
# run: go vet {files}
#
# - script: "hello.js"
# runner: node
#
# - script: "hello.go"
# runner: go run

# Hooks get a minimal PATH; scripts/lefthook-rc.sh loads common Node managers when present.
# For custom setups, copy lefthook-local.yml.example → lefthook-local.yml (gitignored).
rc: scripts/lefthook-rc.sh

pre-commit:
parallel: true
commands:
code-workspace:
run: corepack pnpm vsh code-workspace --auto-commit
lint-vue:
run: corepack pnpm oxfmt {staged_files} && corepack pnpm oxlint --fix --no-error-on-unmatched-pattern {staged_files} && corepack pnpm eslint --cache --fix {staged_files} && corepack pnpm stylelint --fix --allow-empty-input {staged_files}
glob: '*.vue'
lint-js:
run: corepack pnpm oxfmt {staged_files} && corepack pnpm oxlint --fix --no-error-on-unmatched-pattern {staged_files} && corepack pnpm eslint --cache --fix {staged_files}
glob: '*.{js,jsx,ts,tsx}'
exclude:
- 'apps/portal/src/protos-ts/**'
lint-style:
run: corepack pnpm oxfmt {staged_files} && corepack pnpm stylelint --fix --allow-empty-input {staged_files}
glob: '*.{scss,less,styl,html,vue,css}'
lint-package:
run: corepack pnpm oxfmt {staged_files}
glob: 'package.json'
lint-json:
run: corepack pnpm oxfmt {staged_files}
glob: '{!(package)*.json,*.code-snippets,.!(browserslist)*rc}'

post-merge:
commands:
install:
run: corepack pnpm install
# commit-msg:
# commands:
# commitlint:
# run: pnpm exec commitlint --edit $1

設定逐項說明

rc: scripts/lefthook-rc.sh

Git hook 執行時的 PATH 很精簡,常常找不到 nodepnpmnvm 等。rc 會在每個 hook 執行前 source 這支 script,把常見 Node 版本管理器的路徑載入。若你有特殊環境,可複製 lefthook-local.yml.example 做本地調整。

pre-commitparallel: true

所有 command 會同時執行,縮短 commit 等待時間。各 command 依 glob 只處理自己負責的 staged 檔案,互不影響。

Command負責檔案做了什麼
code-workspace無 glob(每次都跑)vsh code-workspace --auto-commit:維護 VS Code workspace 設定並自動 commit
lint-vue*.vueoxfmt → oxlint --fix → eslint --fix → stylelint --fix
lint-js*.{js,jsx,ts,tsx}oxfmt → oxlint --fix → eslint --fix;排除自動產生的 protos-ts
lint-style*.{scss,less,styl,html,vue,css}oxfmt → stylelint --fix
lint-packagepackage.jsonoxfmt 格式化
lint-json其他 json / rc 檔oxfmt;glob 用進階 pattern 排除 package.jsonbrowserslist rc

幾個設計重點:

  • corepack pnpm:透過 corepack 確保使用專案指定的 pnpm 版本,避免團隊環境不一致。
  • 工具鏈順序:先 oxfmt(格式化)→ oxlint(快速 lint)→ eslint(深度檢查)→ stylelint(樣式),前面 fix 過的內容後面工具會接著檢查。
  • --no-error-on-unmatched-pattern:沒有符合檔案時不報錯,避免空跑失敗。
  • --allow-empty-input:stylelint 在沒有輸入檔時不會 exit 1。
  • exclude: apps/portal/src/protos-ts/**:protobuf 產生的 TS 不跑 eslint,避免大量自動產生碼被誤改。

post-merge

git pullgit merge 完成後自動執行 corepack pnpm install,確保依賴與 lockfile 同步。切換有不同 package.json 的分支時特別實用。

註解掉的 commit-msg

若團隊要強制 Conventional Commits,可取消註解,用 commitlint 驗證 $1(commit message 檔案路徑):

commit-msg:
commands:
commitlint:
run: pnpm exec commitlint --edit $1

檔案頂部註解範例

註解區塊是官方風格的參考模板,展示 pre-push + jobs + tags、多語言 lint、以及 script / runner 寫法。實際專案目前未啟用,但可作為擴充起點(例如加上 pre-push 跑 audit 或測試)。

這份設定的執行時序

與 Husky 的對照

若你目前使用 husky,遷移思路如下:

  1. pnpm add -D lefthook,移除 huskylint-staged(若不再需要)
  2. .husky/pre-commit 裡的指令改寫進 lefthook.yml
  3. lefthook install,刪除或停用 .husky/ 目錄
  4. package.jsonpreparehusky install 改為 lefthook install

Husky 適合簡單、純 Node 的 hook;專案變大、要多語言或多任務平行時,Lefthook 通常更好維護。

常用 CLI 速查

lefthook install # 安裝 / 更新 git hooks
lefthook run pre-commit # 手動跑 pre-commit
lefthook validate # 驗證 yml 語法
lefthook dump # 輸出合併後的完整設定(含 extends / remote)
LEFTHOOK=0 git commit # 跳過所有 lefthook hooks

參考連結